from pathlib import Path from typing import Union from talon import Context, Module, actions, app, fs, settings from ..modes.code_languages import code_languages from .snippet_types import ( InsertionSnippet, Snippet, SnippetLanguageState, SnippetLists, WrapperSnippet, ) from .snippets_parser import create_snippets_from_file SNIPPETS_DIR = Path(__file__).parent / "snippets" mod = Module() mod.list("snippet", "List of insertion snippets") mod.list("snippet_with_phrase", "List of insertion snippets containing a text phrase") mod.list("snippet_wrapper", "List of wrapper snippets") mod.setting( "snippets_dir", type=str, default=None, desc="Directory (relative to Talon user) containing additional snippets", ) # `_` represents the global context, ie snippets available regardless of language GLOBAL_ID = "_" # { SNIPPET_NAME: Snippet[] } snippets_map: dict[str, list[Snippet]] = {} # { LANGUAGE_ID: SnippetLanguageState } languages_state_map: dict[str, SnippetLanguageState] = { GLOBAL_ID: SnippetLanguageState(Context(), SnippetLists()) } # Create a context for each defined language for lang in code_languages: ctx = Context() ctx.matches = f"code.language: {lang.id}" languages_state_map[lang.id] = SnippetLanguageState(ctx, SnippetLists()) def get_setting_dir(): setting_dir = settings.get("user.snippets_dir") if not setting_dir: return None dir = Path(setting_dir) if not dir.is_absolute(): user_dir = Path(actions.path.talon_user()) dir = user_dir / dir return dir.resolve() @mod.action_class class Actions: def get_snippets(name: str) -> list[Snippet]: """Get snippets named """ if name not in snippets_map: raise ValueError(f"Unknown snippet '{name}'") return snippets_map[name] def get_snippet(name: str) -> Snippet: """Get snippet named for the active language""" snippets: list[Snippet] = actions.user.get_snippets(name) return get_preferred_snippet(snippets) def get_insertion_snippets(name: str) -> list[InsertionSnippet]: """Get insertion snippets named """ snippets: list[Snippet] = actions.user.get_snippets(name) return [ InsertionSnippet(s.body, s.insertion_scopes, s.languages) for s in snippets ] def get_insertion_snippet(name: str) -> InsertionSnippet: """Get insertion snippet named for the active language""" snippet: Snippet = actions.user.get_snippet(name) return InsertionSnippet( snippet.body, snippet.insertion_scopes, snippet.languages, ) def get_wrapper_snippets(name: str) -> list[WrapperSnippet]: """Get wrapper snippets named """ snippet_name, variable_name = split_wrapper_snippet_name(name) snippets: list[Snippet] = actions.user.get_snippets(snippet_name) return [to_wrapper_snippet(s, variable_name) for s in snippets] def get_wrapper_snippet(name: str) -> WrapperSnippet: """Get wrapper snippet named for the active language""" snippet_name, variable_name = split_wrapper_snippet_name(name) snippet: Snippet = actions.user.get_snippet(snippet_name) return to_wrapper_snippet(snippet, variable_name) def get_preferred_snippet(snippets: list[Snippet]) -> Snippet: lang: Union[str, set[str]] = actions.code.language() languages = [lang] if isinstance(lang, str) else lang # First try to find a snippet matching the active language for snippet in snippets: if snippet.languages: for snippet_lang in snippet.languages: if snippet_lang in languages: return snippet # Then look for a global snippet for snippet in snippets: if not snippet.languages: return snippet raise ValueError(f"Snippet not available for language '{lang}'") def split_wrapper_snippet_name(name: str) -> tuple[str, str]: index = name.rindex(".") return name[:index], name[index + 1 :] def to_wrapper_snippet(snippet: Snippet, variable_name) -> WrapperSnippet: """Get wrapper snippet named """ var = snippet.get_variable_strict(variable_name) return WrapperSnippet( snippet.body, var.name, var.wrapper_scope, snippet.languages, ) def update_snippets(): global snippets_map snippets = get_snippets_from_files() name_to_snippets: dict[str, list[Snippet]] = {} language_to_lists: dict[str, SnippetLists] = {} for snippet in snippets: # Map snippet names to actual snippets name_to_snippets.setdefault(snippet.name, []).append(snippet) # Map languages to phrase / name dicts for language in snippet.languages or [GLOBAL_ID]: lists = language_to_lists.setdefault(language, SnippetLists()) for phrase in snippet.phrases or []: lists.insertion[phrase] = snippet.name for var in snippet.variables: if var.insertion_formatters: lists.with_phrase[phrase] = snippet.name for var in snippet.variables: for phrase in var.wrapper_phrases or []: lists.wrapper[phrase] = f"{snippet.name}.{var.name}" snippets_map = name_to_snippets update_contexts(language_to_lists) def update_contexts(language_to_lists: dict[str, SnippetLists]): global_lists = language_to_lists[GLOBAL_ID] or SnippetLists() for lang, lists in language_to_lists.items(): if lang not in languages_state_map: print(f"Found snippets for unknown language: {lang}") actions.app.notify(f"Found snippets for unknown language: {lang}") continue state = languages_state_map[lang] insertion = {**global_lists.insertion, **lists.insertion} with_phrase = {**global_lists.with_phrase, **lists.with_phrase} wrapper = {**global_lists.wrapper, **lists.wrapper} updated_lists: dict[str, dict[str, str]] = {} if state.lists.insertion != insertion: state.lists.insertion = insertion updated_lists["user.snippet"] = insertion if state.lists.with_phrase != with_phrase: state.lists.with_phrase = with_phrase updated_lists["user.snippet_with_phrase"] = with_phrase if state.lists.wrapper != wrapper: state.lists.wrapper = wrapper updated_lists["user.snippet_wrapper"] = wrapper if updated_lists: state.ctx.lists.update(updated_lists) def get_snippets_from_files() -> list[Snippet]: setting_dir = get_setting_dir() result = [] for file in SNIPPETS_DIR.glob("**/*.snippet"): result.extend(create_snippets_from_file(file)) if setting_dir: for file in setting_dir.glob("**/*.snippet"): result.extend(create_snippets_from_file(file)) return result def on_ready(): fs.watch(SNIPPETS_DIR, lambda _path, _flags: update_snippets()) if get_setting_dir(): fs.watch(get_setting_dir(), lambda _path, _flags: update_snippets()) update_snippets() app.register("ready", on_ready)