Source code for xonsh.completers.emoji

"""Completer for unicode symbols and emoji.

Trigger prefixes are configurable via environment variables:

- ``$XONSH_COMPLETER_EMOJI_PREFIX`` (default ``"::"``) — colorful emoji (faces, animals, objects).
- ``$XONSH_COMPLETER_SYMBOLS_PREFIX`` (default ``":::"``) — simple unicode symbols (arrows, math, dingbats).

Set to empty string to disable.
"""

import unicodedata

from xonsh.built_ins import XSH
from xonsh.completers.tools import RichCompletion, contextual_command_completer
from xonsh.parsers.completion_context import CommandContext

# Colorful emoji (width=2 only)
_EMOJI_RANGES = [
    (0x1F300, 0x1F5FF),  # Misc symbols and pictographs
    (0x1F600, 0x1F64F),  # Emoticons
    (0x1F680, 0x1F6FF),  # Transport and map
    (0x1F900, 0x1F9FF),  # Supplemental symbols
    (0x1FA70, 0x1FAFF),  # Symbols extended-A
]

# Simple unicode symbols
_SYMBOL_RANGES = [
    (0x2190, 0x21FF),  # Arrows
    (0x2200, 0x22FF),  # Mathematical operators
    (0x2300, 0x23FF),  # Miscellaneous technical
    (0x25A0, 0x25FF),  # Geometric shapes
    (0x2600, 0x26FF),  # Miscellaneous symbols
    (0x2700, 0x27BF),  # Dingbats
    (0x2B00, 0x2BFF),  # Misc symbols and arrows
]

_EMOJI_CACHE: list[tuple[str, str]] | None = None
_SYMBOL_CACHE: list[tuple[str, str]] | None = None


[docs] def get_emoji_cache(): """Return the cached list of ``(char, unicode-name)`` pairs for colorful emoji. The cache covers Unicode ranges for emoticons, misc pictographs, transport, supplemental and extended-A symbols, filtered to double-width characters (``wcwidth == 2``). It is built lazily on first call and reused thereafter; callers can use it directly — e.g. to wire a random-emoji prompt field. Returns ------- list of tuple of (str, str) Pairs ``(emoji_character, lowercased_unicode_name)``. Examples -------- >>> import random >>> from xonsh.completers.emoji import get_emoji_cache >>> random.choice(get_emoji_cache())[0] # doctest: +SKIP '🥗' """ global _EMOJI_CACHE if _EMOJI_CACHE is None: from wcwidth import wcwidth _EMOJI_CACHE = [] for start, end in _EMOJI_RANGES: for cp in range(start, end + 1): ch = chr(cp) if wcwidth(ch) != 2: continue try: _EMOJI_CACHE.append((ch, unicodedata.name(ch).lower())) except ValueError: pass return _EMOJI_CACHE
[docs] def get_symbol_cache(): """Return the cached list of ``(char, unicode-name)`` pairs for simple symbols. The cache covers Unicode ranges for arrows, mathematical operators, misc technical symbols, geometric shapes, dingbats, and misc symbols+arrows, filtered to single-width characters (``wcwidth == 1``). It is built lazily on first call and reused thereafter. Returns ------- list of tuple of (str, str) Pairs ``(symbol_character, lowercased_unicode_name)``. Examples -------- >>> from xonsh.completers.emoji import get_symbol_cache >>> any(name == 'rightwards arrow' for _, name in get_symbol_cache()) True """ global _SYMBOL_CACHE if _SYMBOL_CACHE is None: from wcwidth import wcwidth _SYMBOL_CACHE = [] for start, end in _SYMBOL_RANGES: for cp in range(start, end + 1): ch = chr(cp) if wcwidth(ch) != 1: continue try: _SYMBOL_CACHE.append((ch, unicodedata.name(ch).lower())) except ValueError: pass return _SYMBOL_CACHE
def _search(cache, query, prefix_len=0): """Search an emoji/symbol cache by query and return a set of completions. The search is word-aware and case-insensitive against the lowercased Unicode names stored in *cache*. An empty *query* yields up to 200 characters from the cache as-is. Non-empty *query* first collects prefix matches on any whitespace-separated word of the name, then — if fewer than 50 matches — appends substring matches. Parameters ---------- cache : list of tuple of (str, str) Output of :func:`get_emoji_cache` or :func:`get_symbol_cache`. query : str Lowercased search query. Use ``""`` to browse the whole cache. prefix_len : int, optional Length of the completion prefix to replace in the line buffer. Defaults to ``0`` (insertion mode). Returns ------- set of RichCompletion or None Completion set ready to return from a completer, or ``None`` if nothing matched. Examples -------- >>> from xonsh.completers.emoji import get_symbol_cache, _search >>> hits = _search(get_symbol_cache(), "arrow") >>> any("arrow" in c.description for c in hits) True """ results = [] seen = set() if not query: for ch, name in cache: if ch not in seen: seen.add(ch) results.append( RichCompletion( ch, display=ch, description=name, prefix_len=prefix_len ) ) if len(results) >= 200: break else: # Prefix matches first for ch, name in cache: if any(w.startswith(query) for w in name.split()) and ch not in seen: seen.add(ch) results.append( RichCompletion( ch, display=ch, description=name, prefix_len=prefix_len ) ) # Substring matches if len(results) < 50: for ch, name in cache: if query in name and ch not in seen: seen.add(ch) results.append( RichCompletion( ch, display=ch, description=name, prefix_len=prefix_len ) ) return set(results) if results else None def _find_trigger(raw_prefix, trigger): """Find trigger in raw_prefix and return query after it, or None.""" if not trigger: return None idx = raw_prefix.find(trigger) if idx == -1: return None return raw_prefix[idx + len(trigger) :].lower()
[docs] @contextual_command_completer def complete_emoji(ctx: CommandContext): """Complete emoji and unicode symbols using configurable trigger prefixes.""" prefix = ctx.prefix raw_prefix = ctx.opening_quote + prefix env = XSH.env or {} symbol_trigger = env.get("XONSH_COMPLETER_SYMBOLS_PREFIX") or "" emoji_trigger = env.get("XONSH_COMPLETER_EMOJI_PREFIX") or "" # Check longer trigger first to avoid prefix conflict if len(symbol_trigger) >= len(emoji_trigger): triggers = [ (symbol_trigger, get_symbol_cache), (emoji_trigger, get_emoji_cache), ] else: triggers = [ (emoji_trigger, get_emoji_cache), (symbol_trigger, get_symbol_cache), ] for trigger, get_cache in triggers: query = _find_trigger(raw_prefix, trigger) if query is not None: return _search(get_cache(), query, len(prefix)) return None