packages/happy-app/sources/docs/autocomplete-text-manipulation.md
Based on analysis of the Openland Apps repository, this document explains how text manipulation works for autocomplete functionality in both web and mobile implementations.
The foundation of the autocomplete system is the findActiveWord utility that detects when a user is typing a mention (@) or emoji (:).
// packages/openland-y-utils/findActiveWord.ts
const stoplist = ['\n', ',', '(', ')'];
const prefixes = ['@', ':'];
function findActiveWord(content: string, selection: { start: number, end: number }): string | undefined {
if (selection.start !== selection.end) {
return undefined; // No active word if text is selected
}
let startIndex = findActiveWordStart(content, selection);
let res = content.substring(startIndex, selection.end);
if (res.length === 0) {
return undefined;
} else {
return res;
}
}
The algorithm:
The web implementation uses Quill.js rich text editor with custom formats for mentions and emojis.
// packages/openland-web/components/unicorn/URickInput.tsx
// Extract active word from Quill editor
function extractActiveWord(quill: QuillType.Quill) {
let selection = quill.getSelection();
if (!selection) {
return null;
}
let start = Math.max(0, selection.index - 64); // Maximum lookback
return findActiveWord(
quill.getText(start, selection.index + selection.length - start),
{
start: selection.index,
end: selection.index + selection.length,
}
);
}
When a user selects a mention from the autocomplete suggestions:
// URickInput.tsx - commitSuggestion method
commitSuggestion: (type: 'mention' | 'emoji', src: MentionToSend | { name: string; value: string }) => {
let ed = editor.current;
if (ed) {
let selection = ed.getSelection(true);
let autocompleteWord = extractActiveWord(ed);
if (autocompleteWord) {
// Insert the mention/emoji embed at current position
ed.insertEmbed(selection.index, type, src, 'user');
// Add space after mention (not emoji)
if (type === 'mention') {
ed.insertText(selection.index + 1, ' ', 'user');
}
// Delete the typed text (including the @ prefix)
ed.deleteText(
selection.index - autocompleteWord.length,
autocompleteWord.length + selection.length,
'user'
);
// Move cursor after the inserted mention/emoji
ed.setSelection(selection.index + 1, 1, 'user');
}
}
}
The process:
The editor monitors text changes and updates autocomplete suggestions:
q.on('editor-change', () => {
// ... other logic
if (props.onAutocompleteWordChange && props.autocompletePrefixes) {
let selection = q.getSelection();
if (selection) {
let autocompleteWord: string | null = null;
let activeWord = extractActiveWord(q);
if (activeWord) {
// Check if active word starts with any prefix
for (let p of props.autocompletePrefixes) {
if (activeWord.toLowerCase().startsWith(p)) {
autocompleteWord = activeWord;
break;
}
}
}
// Notify parent component of autocomplete word change
if (lastAutocompleteText !== autocompleteWord) {
lastAutocompleteText = autocompleteWord;
props.onAutocompleteWordChange(autocompleteWord);
}
}
}
});
Mobile uses standard React Native TextInput with selection tracking:
// packages/openland-mobile/pages/main/components/MessageInputInner.tsx
<TextInput
ref={ref}
selectionColor={theme.accentPrimary}
style={{...}}
onChangeText={props.onChangeText}
onSelectionChange={props.onSelectionChange}
value={props.text}
multiline={true}
{...inputProps}
/>
While the exact mobile text replacement code wasn't found in the examined files, the pattern follows:
onSelectionChangefindActiveWord with current text and selection// Pseudo-code for mobile text replacement
const replaceText = (text: string, selection: Selection, mention: string) => {
const activeWord = findActiveWord(text, selection);
if (activeWord) {
const startIndex = selection.start - activeWord.length;
const newText =
text.substring(0, startIndex) +
mention + ' ' +
text.substring(selection.end);
return {
text: newText,
selection: { start: startIndex + mention.length + 1, end: startIndex + mention.length + 1 }
};
}
};
Mobile shows suggestions in a floating view above the keyboard:
// packages/openland-mobile/pages/main/components/MessageInputBar.tsx
{props.suggestions && (
<ZBlurredView intensity="normal" style={{ position: 'absolute', bottom: '100%', left: 0, right: 0 }}>
{props.suggestions}
</ZBlurredView>
)}
findActiveWord detects the prefix and extracts the queryThis architecture provides a responsive autocomplete experience across both web and mobile platforms while handling the complexity of text manipulation and cursor management.