init commit

This commit is contained in:
unknown
2025-08-19 08:06:37 -04:00
commit 2957b5515a
743 changed files with 45495 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
# Snippets
Custom format to represent snippets.
## Features
- Custom file ending `.snippet`.
- Supports syntax highlighting in VSCode via an [extension](https://marketplace.visualstudio.com/items?itemName=AndreasArvidsson.andreas-talon)
- Supports auto-formatting in VSCode via an [extension](https://marketplace.visualstudio.com/items?itemName=AndreasArvidsson.andreas-talon)
- Support for insertion and wrapper snippets. Note that while the snippet file syntax here supports wrapper snippets, you will need to install [Cursorless](https://www.cursorless.org) for wrapper snippets to work.
- Support for phrase formatters.
## Format
- A `.snippet` file can contain multiple snippet documents separated by `---`.
- Each snippet document has a context and body separated by `-`.
- Optionally a file can have a single context at the top with no body. This is not a snippet in itself, but default values to be inherited by the other snippet documents in the same file.
- Some context keys supports multiple values. These values are separated by `|`.
- For most keys like `language` or `phrase` multiple values means _or_. You can use phrase _1_ or phrase _2_. The snippet is active in language _A_ or language _B_.
- For `insertionFormatter` multiple values means that the formatters will be applied in sequence.
### Context fields
| Key | Required | Multiple values | Example |
| -------------- | -------- | --------------- | ------------------------------ |
| name | Yes | No | `name: ifStatement` |
| description | No | No | `description: My snippet` |
| language | No | Yes | `language: javascript \| java` |
| phrase | No | Yes | `phrase: if \| if state` |
| insertionScope | No | Yes | `insertionScope: statement` |
- `name`: Unique name identifying the snippets. Can be referenced in Python to use the snippet programmatically.
- `description`: A description of the snippet.
- `language`: Language identifier indicating which language the snippet is available for. If omitted the snippet is enabled globally.
- `phrase`: The spoken phrase used to insert the snippet. eg `"snip if"`.
- `insertionScope`: Used by [Cursorless](https://www.cursorless.org) to infer scope when inserting the snippet. eg `"snip if after air"` gets inferred as `"snip if after state air"`.
### Variables
It's also possible to set configuration that applies to a specific tab stop (`$0`) or variable (`$try`):
| Key | Required | Multiple values | Example |
| ------------------ | -------- | --------------- | ----------------------------------- |
| insertionFormatter | No | Yes | `$0.insertionFormatter: SNAKE_CASE` |
| wrapperPhrase | No | Yes | `$0.wrapperPhrase: try \| trying` |
| wrapperScope | No | No | `$0.wrapperScope: statement` |
- `insertionFormatter`: Formatter to apply to the phrase when inserting the snippet. eg `"snip funk get value"`. If omitted no trailing phrase is available for the snippet.
- `wrapperPhrase`: Used by [Cursorless](https://www.cursorless.org) as the spoken form for wrapping with the snippet. eg `"if wrap air"`. Without Cursorless this spoken form is ignored by Talon.
- `wrapperScope`: Used by [Cursorless](https://www.cursorless.org) to infer scope when wrapping with the snippet. eg `"if wrap air"` gets inferred as `"if wrap state air"`.
## Formatting and syntax highlighting
To get formatting, code completion and syntax highlighting for `.snippet` files: install [andreas-talon](https://marketplace.visualstudio.com/items?itemName=AndreasArvidsson.andreas-talon)
## Examples
### Single snippet definition
![snippets1](./images/snippets1.png)
### Multiple snippet definitions in single file
![snippets2](./images/snippets2.png)
### Default context and multiple values
![snippets3](./images/snippets3.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+66
View File
@@ -0,0 +1,66 @@
from dataclasses import dataclass
from talon import Context
class SnippetLists:
insertion: dict[str, str]
with_phrase: dict[str, str]
wrapper: dict[str, str]
def __init__(self):
self.insertion = {}
self.with_phrase = {}
self.wrapper = {}
@dataclass
class SnippetLanguageState:
ctx: Context
lists: SnippetLists
@dataclass
class SnippetVariable:
name: str
insertion_formatters: list[str] | None = None
wrapper_phrases: list[str] | None = None
wrapper_scope: str | None = None
@dataclass
class Snippet:
name: str
body: str
description: str | None
phrases: list[str] | None
insertion_scopes: list[str] | None
languages: list[str] | None
variables: list[SnippetVariable]
def get_variable(self, name: str):
for var in self.variables:
if var.name == name:
return var
return None
def get_variable_strict(self, name: str):
variable = self.get_variable(name)
if variable is None:
raise ValueError(f"Snippet '{self.name}' has no variable '{name}'")
return variable
@dataclass
class InsertionSnippet:
body: str
scopes: list[str] | None
languages: list[str] | None
@dataclass
class WrapperSnippet:
body: str
variable_name: str
scope: str | None
languages: list[str] | None
+224
View File
@@ -0,0 +1,224 @@
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 <name>"""
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 <name> 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 <name>"""
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 <name> 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 <name>"""
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 <name> 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 <name>"""
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)
+6
View File
@@ -0,0 +1,6 @@
snip {user.snippet}: user.insert_snippet_by_name(snippet)
snip {user.snippet_with_phrase} <user.text>:
user.insert_snippet_by_name_with_phrase(snippet_with_phrase, text)
snip next: user.move_cursor_to_next_snippet_stop()
@@ -0,0 +1,19 @@
name: breakStatement
phrase: break
insertionScope: statement
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | rust
-
break;
---
language: go | python | scala | stata | r
-
break
---
language: lua
-
break $0
---
@@ -0,0 +1,9 @@
name: breakWithStatement
phrase: break with
insertionScope: statement
---
language: javascript | typescript | javascriptreact | typescriptreact | php | rust
-
break $0;
---
@@ -0,0 +1,18 @@
language: c | cpp
---
name: typedefStructDeclaration
phrase: typedef struct
$0.insertionFormatter: PUBLIC_CAMEL_CASE
insertionScope: statement
-
typedef struct {
$1
} $0
---
name: preprocessorPragmaStatement
phrase: pre pragma | pragma
insertionScope: statement
-
#pragma $0
---
@@ -0,0 +1,44 @@
name: caseStatement
phrase: case
insertionScope: branch
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact
-
case $1:
$0
---
language: python | php | go
-
case $1:
$0
---
language: ruby
-
when $1
$0
---
language: scala
-
case $1 => $0
---
language: elixir
-
$1 ->
$0
---
language: r
-
$1 ~ $2,
)
---
language: kotlin
-
$1 -> $0
---
@@ -0,0 +1,46 @@
name: catchStatement
phrase: catch
---
language: cpp
-
catch (const std::exception& ex) {
$0
}
---
language: java | csharp
-
catch(final Exception ex) {
$0
}
---
language: javascript | typescript | javascriptreact | typescriptreact
-
catch(error) {
$0
}
---
language: python
-
except Exception as ex:
$0
---
language: php
-
catch (\Throwable \$exception) {
$0
}
---
language: r
-
warning = function(w) {
$1
}, error = function(e) {
$0
},
---
@@ -0,0 +1,49 @@
name: classDeclaration
phrase: class
insertionScope: class | statement
$0.wrapperPhrase: class
$0.wrapperScope: statement
---
language: csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | kotlin
$1.insertionFormatter: PUBLIC_CAMEL_CASE
-
class $1 {
$0
}
---
language: python
$1.insertionFormatter: PUBLIC_CAMEL_CASE
-
class $1:
$0
---
language: ruby
$1.insertionFormatter: PUBLIC_CAMEL_CASE
-
class $1
$0
end
---
language: scala
$1.insertionFormatter: PUBLIC_CAMEL_CASE
-
class $1 ($0)
---
language: cpp
$1.insertionFormatter: PUBLIC_CAMEL_CASE
-
class $1 {
$0
};
---
@@ -0,0 +1,23 @@
name: codeBlock
phrase: block
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | scala | kotlin | r | rust | go | css | scss | terraform | protobuf
-
{
$0
}
---
language: python
-
:
$0
---
language: stata
-
$0
---
@@ -0,0 +1,28 @@
name: commentBlock
phrase: block comment
insertionScope: statement
$0.insertionFormatter: CAPITALIZE_FIRST_WORD
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | scala | kotlin
-
/* $0 */
---
language: python
-
"""$0"""
---
language: xml | html
-
<!-- $0 -->
---
language: lua
-
--[[
$0
--]]
---
@@ -0,0 +1,21 @@
name: commentDocumentation
phrase: doc comment | doc string
insertionScope: statement
$0.insertionFormatter: CAPITALIZE_FIRST_WORD
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php
-
/** $0 */
---
language: python
-
"""$0"""
---
language: rust
-
/// $0
---
@@ -0,0 +1,45 @@
name: commentLine
phrase: comment
$0.insertionFormatter: CAPITALIZE_FIRST_WORD
---
language: c | cpp | csharp | json | java | javascript | typescript | javascriptreact | typescriptreact | php | go | scala | kotlin | rust
-
// $0
---
language: python | talon | csv | elixir | terraform | ruby | r
-
# $0
---
language: xml | html | markdown
-
<!-- $0 -->
---
language: vimscript
-
"$0
---
language: r
-
#$0
---
language: stata
-
* $0
---
language: sql | lua
-
-- $0
---
language: batch
-
REM $0
---
@@ -0,0 +1,24 @@
name: constructorDeclaration
phrase: constructor
insertionScope: namedFunction | statement
---
language: cpp | csharp | java
-
$1($2) {
$0
}
---
language: javascript | typescript | javascriptreact | typescriptreact
-
constructor($1) {
$0
}
---
language: python
-
def __init__(self$1):
$0
---
@@ -0,0 +1,24 @@
name: continueStatement
phrase: continue
insertionScope: statement
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | rust
-
continue;
---
language: go | python | scala | stata
-
continue
---
language: lua
-
goto continue
---
language: r
-
next
---
@@ -0,0 +1,9 @@
name: continueWithLabelStatement
phrase: continue with
insertionScope: statement
---
language: javascript | typescript | javascriptreact | typescriptreact | php | rust
-
continue $0;
---
@@ -0,0 +1,37 @@
name: defaultStatement
phrase: default
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | go
-
default:
$0
---
language: python
-
case _:
$0
---
language: ruby
-
else
$0
---
language: scala
-
case _ => $0
---
language: elixir
-
_ ->
$0
---
language: kotlin
-
else -> $0
---
@@ -0,0 +1,33 @@
name: doWhileLoopStatement
phrase: do while
insertionScope: statement
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact
-
do {
$0
} while($1);
---
language: lua
-
repeat
$0
until $1
---
language: php
-
do {
$0
} while ($1);
---
language: ruby
-
loop do
$0
break if $1
end
---
@@ -0,0 +1,12 @@
language: elixir
---
name: conditionStatement
phrase: cond
insertionScope: statement
-
cond do
$1 ->
$0
end
---
@@ -0,0 +1,54 @@
name: elseIfStatement
phrase: elif
insertionScope: statement
$1.wrapperPhrase: elif cond
$1.wrapperScope: statement
$0.wrapperPhrase: elif
$0.wrapperScope: statement
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | scala | kotlin | r
-
else if ($1) {
$0
}
---
language: python
-
elif $1:
$0
---
language: lua
-
elseif $1 then
$0
---
language: ruby
-
elsif $1
$0
---
language: vimscript
-
elseif $1
$0
---
language: php
-
elseif ($1) {
$0
}
---
language: stata | go | rust
-
else if $1 {
$0
}
---
@@ -0,0 +1,38 @@
name: elseStatement
phrase: else
insertionScope: statement
$0.wrapperPhrase: else
$0.wrapperScope: statement
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | r | php | stata | go | scala | kotlin | rust
-
else {
$0
}
---
language: python
-
else:
$0
---
language: lua | elixir
-
else
$0
---
language: vimscript
-
else
$0
---
language: ruby
-
else
$0
---
@@ -0,0 +1,24 @@
name: finallyStatement
phrase: finally
---
language: csharp | java | javascript | typescript | javascriptreact | typescriptreact
-
finally {
$0
}
---
language: python
-
finally:
$0
---
language: r
-
finally = {
$0
})
---
@@ -0,0 +1,25 @@
name: forEachMutableStatement
phrase: for each mute | each mute
insertionScope: statement
---
language: cpp
-
for (auto &$1 : $2) {
$0
}
---
language: java
-
for ($1 : $2) {
$0
}
---
language: javascript | typescript | javascriptreact | typescriptreact
-
for ($1 of $2) {
$0
}
---
@@ -0,0 +1,104 @@
name: forEachStatement
phrase: for each | each
insertionScope: statement
---
language: cpp
-
for (const auto &$1 : $2) {
$0
}
---
language: csharp
-
foreach ($1 in $2) {
$0
}
---
language: java
-
for (final $1 : $2) {
$0
}
---
language: javascript | typescript | javascriptreact | typescriptreact
-
for (const $1 of $2) {
$0
}
---
language: python
-
for $1 in $2:
$0
---
language: rust
-
for $1 in $2 {
$0
}
---
language: lua
-
for $1 in $2 do
$0
end
---
language: ruby
-
.each do |$1|
$0
end
---
language: scala
-
for ($1 <- $0)
---
language: terraform
-
for $1 in $2 : $0
---
language: go
-
for $1 := range $2 {
$0
}
---
language: php
-
foreach ($1 as $2) {
$0
}
---
language: r | kotlin
-
for ($1 in $2) {
$0
}
---
language: stata
-
foreach $1 in $2 {
$0
}
---
language: r
-
for ($1 in $2) {
$0
}
---
@@ -0,0 +1,69 @@
name: forLoopStatement
phrase: for
insertionScope: statement
---
language: scala
-
for ($1) $0
---
language: cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact
-
for ($1) {
$0
}
---
language: php | c
-
for ($1; $2; $3) {
$0
}
---
language: stata
-
forval $1 {
$0
}
---
language: elixir | lua
-
for $1 do
$0
end
---
language: rust
-
for $1 in $2 {
$0
}
---
language: go
-
for $1 {
$0
}
---
language: r | kotlin
-
for ($1 in $2) {
$0
}
---
language: python
-
for $1 in $2:
$0
---
language: terraform
-
for $1 in $2 : $0
---
@@ -0,0 +1,66 @@
name: forRangeStatement
phrase: for range
insertionScope: statement
---
language: c | csharp | java
-
for (int i = 0; i < $1; ++i) {
$0
}
---
language: cpp
-
for (size_t i = 0; i < $1; ++i) {
$0
}
---
language: javascript | typescript | javascriptreact | typescriptreact
-
for (let i = 0; i < $1; ++i) {
$0
}
---
language: python
-
for i in range($1):
$0
---
language: scala
-
for (i <- 0 until $1) {
$0
}
---
language: go
-
for i := 0; i < $1; i++ {
$0
}
---
language: stata
-
forval $1 {
$0
}
---
language: r
-
for (${1} in 1:${2}) {
$0
}
---
language: rust
-
for i in 0..$1 {
$0
}
---
@@ -0,0 +1,33 @@
name: formatString
phrase: format
---
language: cpp
-
std::format("$0")
---
language: csharp
-
String.Format("$0")
---
language: java
-
String.format("$0")
---
language: javascript | typescript | javascriptreact | typescriptreact
-
`$0`
---
language: python
-
f"$0"
---
language: r
-
${1:glue::}glue("$0")
---
@@ -0,0 +1,16 @@
name: functionCall
phrase: call
insertionScope: statement
$0.wrapperPhrase: call
---
language: c | cpp | csharp | python | talon | java | javascript | typescript | javascriptreact | typescriptreact | css | scss | sql | r
-
$1($0)
---
language: stata
-
$1 $0
---
@@ -0,0 +1,75 @@
name: functionDeclaration
phrase: funk
insertionScope: namedFunction | statement
$0.wrapperPhrase: funk
$0.wrapperScope: statement
---
language: c | cpp | csharp | java
$1.insertionFormatter: PRIVATE_CAMEL_CASE
-
void $1($2) {
$0
}
---
language: javascript | typescript | javascriptreact | typescriptreact
$1.insertionFormatter: PRIVATE_CAMEL_CASE
-
function $1($2) {
$0
}
---
language: python
$1.insertionFormatter: SNAKE_CASE
-
def $1($2):
$0
---
language: rust
$1.insertionFormatter: SNAKE_CASE
-
fn $1($2) {
$0
}
---
language: ruby
$1.insertionFormatter: SNAKE_CASE
-
def $1($2)
$0
end
---
language: vimscript
-
function $1($2)
$0
endfunction
---
language: php
$1.insertionFormatter: SNAKE_CASE
-
function $1($2) {
$0
}
---
language: r
$1.insertionFormatter: SNAKE_CASE
-
$1 <- function($2){
$0
}
---
@@ -0,0 +1,15 @@
name: codeQuote
phrase: code
-
```
$0
```
---
name: codeQuoteLanguage
phrase: code lang
-
```$1
$0
```
---
@@ -0,0 +1,14 @@
name: goToStatement
phrase: go to
insertionScope: statement
---
language: c | cpp | csharp | php
-
goto $0;
---
language: lua
-
goto $0
---
@@ -0,0 +1,33 @@
language: html | javascriptreact | typescriptreact
---
name: attributeClassName
phrase: class name
-
className="$0"
---
name: table
phrase: table
-
<table>
<thead>
<tr>
<th>$1</th>
</tr>
</thead>
<tbody>
<tr>
<td>$0</td>
</tr>
</tbody>
</table>
---
name: unorderedList
phrase: list
-
<ul>
<li>$0</li>
</ul>
---
@@ -0,0 +1,71 @@
name: ifElseStatement
phrase: if else
insertionScope: statement
$1.wrapperPhrase: if else cond
$1.wrapperScope: statement
$2.wrapperPhrase: if else
$2.wrapperScope: statement
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | scala | kotlin | r
-
if ($1) {
$2
} else {
$0
}
---
language: python
-
if $1:
$2
else:
$0
---
language: lua
-
if $1 then
$2
else
$0
end
---
language: ruby
-
if $1
$2
else
$0
end
---
language: vimscript
-
if $1
$2
else
$0
endif
---
language: rust | stata | go
-
if $1 {
$2
} else {
$0
}
---
language: elixir
-
if $1 do
$2
else
$0
end
---
@@ -0,0 +1,57 @@
name: ifStatement
phrase: if
insertionScope: statement
$1.wrapperPhrase: if cond
$1.wrapperScope: statement
$0.wrapperPhrase: if
$0.wrapperScope: statement
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | scala | kotlin | r
-
if ($1) {
$0
}
---
language: python
-
if $1:
$0
---
language: lua
-
if $1 then
$0
end
---
language: ruby
-
if $1
$0
end
---
language: vimscript
-
if $1
$0
endif
---
language: rust | stata | go
-
if $1 {
$0
}
---
language: elixir
-
if $1 do
$0
end
---
@@ -0,0 +1,64 @@
name: importStatement
phrase: import
---
language: javascript | typescript | javascriptreact | typescriptreact
-
import $0 from "$0";
---
language: python | go | scala
-
import $0
---
language: lua
-
local $1 = require('$0')
---
language: ruby
-
require "$0"
---
language: java
-
import $0;
---
language: php | rust
-
use $0;
---
language: r
-
library($0)
---
language: stata
-
ssc install $0
---
language: css | scss
-
@import $0
---
language: csharp
-
using $0;
---
language: r
-
library(${1}${2:, warn.conflicts = FALSE})
---
language: c
-
#include $0
---
@@ -0,0 +1,23 @@
name: importFromStatement
phrase: import from
---
language: javascript | typescript | javascriptreact | typescriptreact
-
import $1 from "$0";
---
language: python
-
from $1 import $0
---
language: r
-
library(
${1},
include.only = c(${0})${2:,
warn.conflicts = FALSE}
)
---
@@ -0,0 +1,19 @@
name: importStarStatement
phrase: import star
---
language: javascript | typescript | javascriptreact | typescriptreact
-
import * as $0 from "$0";
---
language: python
-
from $0 import *
---
language: rust
-
use $0::*;
---
@@ -0,0 +1,9 @@
name: includeHeaderStatement
phrase: include header | include head
insertionScope: statement
---
language: c | cpp
-
#include "$0.h"
---
@@ -0,0 +1,9 @@
name: includeLocalStatement
phrase: include local | include low
insertionScope: statement
---
language: c | cpp
-
#include "$0"
---
@@ -0,0 +1,9 @@
name: includeSystemStatement
phrase: include system | include sys
insertionScope: statement
---
language: c | cpp
-
#include <$0>
---
@@ -0,0 +1,11 @@
name: infiniteLoopStatement
phrase: loop
insertionScope: statement
---
language: rust
-
loop {
$0
}
---
@@ -0,0 +1,19 @@
name: item
phrase: item
insertionScope: collectionItem
---
language: javascript | typescript | javascriptreact | typescriptreact
-
$1: $0,
---
language: python
-
"$1": $0,
---
language: r
-
${1}="$0",
---
@@ -0,0 +1,53 @@
language: javascript | typescript | javascriptreact | typescriptreact
---
name: forInLoopStatement
phrase: for in
insertionScope: statement
-
for (const $1 in $2) {
$0
}
---
name: anonymousFunctionDeclarationAndCall
phrase: self calling
-
(() => {
$0
})();
---
name: namedLambdaExpression
phrase: arrow funk
insertionScope: statement
-
const $1 = ($2) => {
$0
}
---
name: printTimeStatement
phrase: print time
insertionScope: statement
-
console.time("$0");
---
name: constAssignment
phrase: const
insertionScope: statement
$1.insertionFormatter: PRIVATE_CAMEL_CASE
-
const $1 = $0;
---
name: letAssignment
phrase: let
insertionScope: statement
$1.insertionFormatter: PRIVATE_CAMEL_CASE
-
let $1 = $0;
---
@@ -0,0 +1,30 @@
language: javascript | javascriptreact | typescriptreact
---
name: attribute
-
$name={$0}
---
name: reactUseState
phrase: use state
insertionScope: statement
-
const [$1, set${1/(.)/${1:/capitalize}/}] = useState($0);
---
name: reactUseRef
phrase: use ref
insertionScope: statement
-
const $0 = useRef();
---
name: reactUseEffect
phrase: use effect
insertionScope: statement
-
useEffect(() => {
$0
}, []);
---
@@ -0,0 +1,38 @@
name: lambdaExpression
phrase: lambda
$0.wrapperPhrase: lambda
$0.wrapperScope: statement
---
language: csharp | javascript | typescript | javascriptreact | typescriptreact
-
($1) => {
$0
}
---
language: java
-
($1) -> {
$0
}
---
language: python
-
lambda $1: $0
---
language: r
-
function($1){ $0 }
---
language: cpp
-
[$1]($2) {
$0
}
---
@@ -0,0 +1,28 @@
language: lua
---
name: forInIPairs
phrase: for eye pairs
insertionScope: statement
$1.insertionFormatter: SNAKE_CASE
-
for _, $1 in ipairs($2) do
$0
end
---
name: forInPairs
phrase: for pairs
insertionScope: statement
-
for ${1:k}, ${2:v} in pairs($3) do
$0
end
---
name: tryCatchStatement
phrase: try catch
insertionScope: statement
-
pcall($0)
---
@@ -0,0 +1,45 @@
language: markdown
---
name: link
phrase: link
-
[$1]($0)
---
name: image
phrase: image
-
![$1]($0)
---
name: checkbox
phrase: to do | check box | tick box
-
* [ ] $0
---
name: bold
phrase: bold | strong
-
**$0**
---
name: italic
phrase: italic | emph | emphasis
-
*$0*
---
name: italic bold
phrase: italic bold | bold italic | strong emphasis | strong emph
-
***$0***
---
name: quote
phrase: quote
-
> $0
---
@@ -0,0 +1,13 @@
name: methodDeclaration
phrase: method
insertionScope: namedFunction | statement
---
language: javascript | typescript | javascriptreact | typescriptreact
$1.insertionFormatter: PRIVATE_CAMEL_CASE
-
$1($2) {
$0
}
---
@@ -0,0 +1,13 @@
name: newInstance
phrase: instance
---
language: cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact
-
new $1($0);
---
language: python
-
$1($0)
---
@@ -0,0 +1,10 @@
name: preprocessorDefineStatement
phrase: pre define | define
insertionScope: statement
---
language: c | cpp
$0.insertionFormatter: ALL_CAPS,SNAKE_CASE
-
#define $0
---
@@ -0,0 +1,9 @@
name: preprocessorElseIfStatement
phrase: pre elif
insertionScope: statement
---
language: c | cpp
-
#elif $0
---
@@ -0,0 +1,9 @@
name: preprocessorEndIfStatement
phrase: pre end if | end if
insertionScope: statement
---
language: c | cpp
-
#endif $0
---
@@ -0,0 +1,9 @@
name: preprocessorErrorStatement
phrase: pre error | error
insertionScope: statement
---
language: c | cpp
-
#error $0
---
@@ -0,0 +1,10 @@
name: preprocessorIfDefineStatement
phrase: pre if deaf | if deaf
insertionScope: statement
---
language: c | cpp
$0.insertionFormatter: ALL_CAPS,SNAKE_CASE
-
#ifdef $0
---
@@ -0,0 +1,9 @@
name: preprocessorIfStatement
phrase: pre if
insertionScope: statement
---
language: c | cpp
-
#if $0
---
@@ -0,0 +1,10 @@
name: preprocessorUndefineStatement
phrase: pre undeaf | undeaf
insertionScope: statement
---
language: c | cpp
$0.insertionFormatter: ALL_CAPS,SNAKE_CASE
-
#undef $0
---
@@ -0,0 +1,36 @@
name: printStatement
phrase: print
insertionScope: statement
$0.wrapperPhrase: print
---
language: cpp
-
std::printf($0);
---
language: java
-
System.out.println($0);
---
language: javascript | typescript | javascriptreact | typescriptreact
-
console.log($0);
---
language: python | talon | r
-
print($0)
---
language: csharp
-
Console.WriteLine($0);
---
language: rust
-
println!($0);
---
@@ -0,0 +1,96 @@
language: python
---
name: talonModuleDeclaration
phrase: module
insertionScope: statement
-
mod = Module()
---
name: talonContextDeclaration
phrase: context
insertionScope: statement
-
ctx = Context()
---
name: talonAppDeclaration
phrase: module app
insertionScope: statement
-
mod.apps.$1 = r"""
$0
"""
---
name: talonModuleClass
phrase: module class
insertionScope: class | statement
-
@mod.action_class
class Actions:
$0
---
name: talonContextMatch
phrase: context match
insertionScope: statement
-
ctx.matches = r"""
$0
"""
---
name: talonContextList
phrase: context list
insertionScope: statement
-
ctx.lists["user.$1"] = {
$0
}
---
name: talonContextClass
phrase: context class
insertionScope: class | statement
-
@ctx.action_class("$1")
class $2Actions:
$0
---
name: suppressError
phrase: suppress error
-
with suppress(AttributeError):
$0
---
name: listComprehension
phrase: list comp
insertionScope: statement
-
[$2 for $2 in $1 if $0]
---
name: setComprehension
phrase: set comp
insertionScope: statement
-
{$0 for $0 in $1}
---
name: dictComprehension
phrase: dict comp
insertionScope: statement
-
{$0: $2 for $2 in $1}
---
name: generatorExpression
phrase: gen comp
insertionScope: statement
-
($2 for $2 in $1 if $0)
---
@@ -0,0 +1,19 @@
name: returnStatement
phrase: return
insertionScope: statement
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | rust
-
return $0;
---
language: python | go | kotlin | lua | ruby | scala
-
return $0
---
language: r
-
return($0)
---
@@ -0,0 +1,104 @@
language: rust
---
name: implementsStruct
phrase: imp | implement
insertionScope: statement
$1.insertionFormatter: PUBLIC_CAMEL_CASE
-
impl $1 {
$0
}
---
name: implementsGenericStruct
phrase: generic imp | generic implement | gen imp | gen implement
insertionScope: statement
$2.insertionFormatter: PUBLIC_CAMEL_CASE
-
impl<$1> $2<$3> {
$0
}
---
name: enumTypeDeclaration
phrase: enum
$1.insertionFormatter: PUBLIC_CAMEL_CASE
-
enum $1 {
$0
}
---
name: traitDeclaration
phrase: trait
$1.insertionFormatter: PUBLIC_CAMEL_CASE
-
trait $1 {
$0
}
---
name: traitImplementation
phrase: implement trait | imp trait
$1.insertionFormatter: PUBLIC_CAMEL_CASE
-
impl $1 for $2 {
$0
}
---
name: ifLetStatement
phrase: if let
insertionScope: statement
-
if let $1 = $2 {
$0
}
---
name: letElseStatement
phrase: let else
insertionScope: statement
-
let $1 = $2 else {
return $0;
};
---
name: unsafeBlock
phrase: unsafe
-
unsafe {
$0
}
---
name: attributeStatement
phrase: attribute | attr
$0.insertionFormatter: SNAKE_CASE
-
#[$0]
---
name: moduleDeclaration
phrase: mod | module
$1.insertionFormatter: SNAKE_CASE
-
mod $1 {
$0
}
---
name: testModuleDeclaration
phrase: test mod | test module
-
#[cfg(test)]
mod tests {
use super::*;
$0
}
---
@@ -0,0 +1,11 @@
language: shellscript
---
name: shebang
phrase: shebang
-
#!/usr/bin/env bash
set -euo pipefail
$0
---
@@ -0,0 +1,20 @@
language: sql
---
name: createTable
phrase: create table
insertionScope: statement
-
CREATE TABLE $1(
$0
);
---
name: withStatement
phrase: with
insertionScope: statement
-
WITH $1 AS (
SELECT $0
)
---
@@ -0,0 +1,19 @@
name: structDeclaration
phrase: struct
$1.insertionFormatter: PUBLIC_CAMEL_CASE
---
language: rust
insertionScope: class
-
struct $1 {
$0
}
---
language: cpp
insertionScope: statement
-
struct $1 {
$0
};
@@ -0,0 +1,67 @@
name: switchStatement
phrase: switch
insertionScope: statement
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php
-
switch ($1) {
$0
}
---
language: python
-
match $1:
$0
---
language: rust
-
match $1 {
$0
}
---
language: kotlin
-
when ($1) {
$0
}
---
language: ruby
-
case $1
$0
end
---
language: scala
-
$1 match {
$0
}
---
language: go
-
switch $1 {
$0
}
---
language: elixir
-
case $1 do
$0
end
---
language: r
-
case_when(
$0
.default = ${1}
)
---
@@ -0,0 +1,11 @@
language: talon
---
name: voiceCommandDeclaration
phrase: command
insertionScope: command
$0.insertionFormatter: NOOP
-
$0: user.run_rpc_command("$CLIPBOARD")
---
@@ -0,0 +1,33 @@
name: ternary
phrase: ternary
---
language: c | cpp | java | csharp | javascript | typescript | javascriptreact | typescriptreact | terraform | ruby
-
$1 ? $2 : $0
---
language: python
-
$1 if $2 else $0
---
language: lua
-
$1 and $2 or $0
---
language: rust
-
if $1 { $2 } else { $0 }
---
language: scala | kotlin
-
if ($1) $2 else $0
---
language: r
-
if ($2) { $1 } else { $0 }
---
@@ -0,0 +1,33 @@
name: throwException
phrase: exception
---
language: cpp
-
throw std::runtime_error("$0");
---
language: java
-
throw new Exception(String.format("$0"));
---
language: csharp
-
throw new Exception(String.Format("$0"));
---
language: javascript | typescript | javascriptreact | typescriptreact
-
throw Error(`$0`);
---
language: python
-
raise ValueError(f"$0")
---
language: r
-
stop($0)
---
@@ -0,0 +1,60 @@
name: tryCatchStatement
phrase: try catch
insertionScope: statement
$1.wrapperPhrase: try
$1.wrapperScope: statement
$0.wrapperPhrase: catch
$0.wrapperScope: statement
---
language: cpp
-
try {
$1
}
catch (const std::exception& e) {
$0
}
---
language: csharp | java
-
try {
$1
}
catch(final Exception ex) {
$0
}
---
language: javascript | typescript | javascriptreact | typescriptreact
-
try {
$1
}
catch(error) {
$0
}
---
language: python
-
try:
$1
except Exception as ex:
$0
---
language: r
-
tryCatch({
$0
}, warning = function(w) {
$1
}, error = function(e) {
$2
}, finally = {
$3
})
---
@@ -0,0 +1,31 @@
name: tryStatement
phrase: try
insertionScope: statement
---
language: cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php
-
try {
$0
}
---
language: python
-
try:
$0
---
language: elixir
-
try do
$0
end
---
language: r
-
tryCatch({
$0
},
---
@@ -0,0 +1,13 @@
language: typescript | typescriptreact
---
name: interfaceDeclaration
phrase: interface
insertionScope: statement
$1.insertionFormatter: PUBLIC_CAMEL_CASE
-
interface $1 {
$0
}
---
@@ -0,0 +1,11 @@
name: unlessStatement
phrase: unless
insertionScope: statement
---
language: ruby
-
unless $1
$0
end
---
@@ -0,0 +1,11 @@
name: untilLoopStatement
phrase: until
insertionScope: statement
---
language: ruby
-
until $1
$0
end
---
@@ -0,0 +1,45 @@
name: whileLoopStatement
phrase: while
insertionScope: statement
---
language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | scala | kotlin | r
-
while ($1) {
$0
}
---
language: python
-
while $1:
$0
---
language: ruby
-
while $1
$0
end
---
language: go
-
for $1 {
$0
}
---
language: rust | stata
-
while $1 {
$0
}
---
language: elixir | lua
-
while $1 do
$0
end
---
@@ -0,0 +1,13 @@
name: withStatement
phrase: with
insertionScope: statement
$0.wrapperPhrase: with
$0.wrapperScope: statement
---
language: python
-
with $1:
$0
---
@@ -0,0 +1,23 @@
language: xml | html | javascriptreact | typescriptreact
---
name: element
phrase: element | tag
insertionScope: xmlElement
$1.insertionFormatter: PRIVATE_CAMEL_CASE
$0.wrapperPhrase: element | tag
$0.wrapperScope: xmlElement
-
<$1>
$0
</$1>
---
name: attribute
phrase: attribute
$1.insertionFormatter: PRIVATE_CAMEL_CASE
-
$1=$0
---
@@ -0,0 +1,56 @@
import re
from talon import Module, actions
from .snippet_types import Snippet
from .snippets_insert_raw_text import go_to_next_stop_raw, insert_snippet_raw_text
mod = Module()
@mod.action_class
class Actions:
def insert_snippet(body: str):
"""Insert snippet"""
insert_snippet_raw_text(body)
def move_cursor_to_next_snippet_stop():
"""Moves the cursor to the next snippet stop"""
go_to_next_stop_raw()
def insert_snippet_by_name(
name: str,
substitutions: dict[str, str] = None,
):
"""Insert snippet <name>"""
snippet: Snippet = actions.user.get_snippet(name)
body = snippet.body
if substitutions:
for k, v in substitutions.items():
reg = re.compile(rf"\${k}|\$\{{{k}\}}")
if not reg.search(body):
raise ValueError(
f"Can't substitute non existing variable '{k}' in snippet '{name}'"
)
body = reg.sub(v, body)
actions.user.insert_snippet(body)
def insert_snippet_by_name_with_phrase(name: str, phrase: str):
"""Insert snippet <name> with phrase <phrase>"""
snippet: Snippet = actions.user.get_snippet(name)
substitutions = {}
for variable in snippet.variables:
if variable.insertion_formatters is not None:
formatters = ",".join(variable.insertion_formatters)
formatted_phrase = actions.user.formatted_text(phrase, formatters)
substitutions[variable.name] = formatted_phrase
if not substitutions:
raise ValueError(
f"Can't use snippet phrase. No variable with insertion formatter in snippet '{name}'"
)
actions.user.insert_snippet_by_name(name, substitutions)
@@ -0,0 +1,219 @@
import re
from collections import defaultdict
from dataclasses import dataclass
from talon import Module, actions, app, settings
mod = Module()
mod.setting(
"snippet_raw_text_spaces_per_tab",
type=int,
default=4,
desc="""The number of spaces per tab when inserting snippets as raw text. Set to -1 to insert tabs as tabs, such as in code editors that can expand tabs in pasted or typed text. This setting is provided for applications like web browsers and chat apps.""",
)
mod.setting(
"snippet_raw_text_paste",
type=bool,
default=False,
desc="""If true, inserting snippets as raw text will always be done through pasting""",
)
RE_STOP = re.compile(r"\$(\d+|\w+)|\$\{(\d+|\w+)\}|\$\{(\d+|\w+):(.+)\}")
LAST_SNIPPET_HOLE_KEY_VALUE = 1000
@dataclass
class Stop:
name: str
rows_up: int
columns_left: int
row: int
col: int
def compute_sorting_key(self) -> int:
"""Returns a key value used to sort stops"""
if self.name == "0":
return LAST_SNIPPET_HOLE_KEY_VALUE
if self.name.isdigit():
return int(self.name)
return 999
stop_stack: list[Stop] = []
def go_to_next_stop_raw():
"""Goes to the next snippet stop if it exists"""
global stop_stack
if len(stop_stack) > 1:
current_stop = stop_stack.pop()
next_stop = stop_stack[-1]
move_to_correct_row(current_stop, next_stop)
move_to_correct_column(next_stop)
else:
stop_stack = []
def insert_snippet_raw_text(body: str):
"""Insert snippet as raw text without editor support"""
updated_snippet, stops = parse_snippet(body)
sorted_stops = compute_stops_sorted_always_moving_left_to_right(stops)
stop = get_first_stop(sorted_stops)
update_stop_information(sorted_stops)
if settings.get("user.snippet_raw_text_paste"):
actions.user.paste(updated_snippet)
else:
actions.insert(updated_snippet)
if stop:
up(stop.rows_up)
move_to_correct_column(stop)
def update_stop_information(stops: list[Stop]):
global stop_stack
if len(stops) > 1:
stop_stack = stops[:]
stop_stack.reverse()
else:
stop_stack = []
def compute_stops_sorted_always_moving_left_to_right(stops: list[Stop]) -> list[Stop]:
"""Without editor support, moving from right to left is problematic. Each line of stops is sorted by the smallest snippet hole key in the line. Each line gets sorted from left to right."""
# Separate the stops by line keeping track of the smallest key in each line
lines = defaultdict(list)
smallest_keys = defaultdict(lambda: LAST_SNIPPET_HOLE_KEY_VALUE)
for stop in stops:
lines[stop.row].append(stop)
line_key = smallest_keys[stop.row]
smallest_keys[stop.row] = min(line_key, stop.compute_sorting_key())
# If a line was from right to left, notify user and sort
if is_any_line_from_right_to_left(lines.values()):
app.notify(
"The snippet you inserted got adjusted to move from left to right because editor support is unavailable."
)
sorted_stops: list[Stop] = []
# Sort lines by key
sorted_lines = sorted(
lines.values(), key=lambda line: smallest_keys[line[0].row]
)
# Add every line sorted from left to right
for line in sorted_lines:
sorted_line = sorted(line, key=lambda stop: stop.col)
sorted_stops.extend(sorted_line)
return sorted_stops
return sorted(stops, key=lambda stop: stop.compute_sorting_key())
def is_any_line_from_right_to_left(lines) -> bool:
for line in lines:
# Lines with only one stop are always in order
if len(line) > 1:
stop = line[0]
stop_key = stop.compute_sorting_key()
for next_stop in line[1:]:
next_key = next_stop.compute_sorting_key()
# If the ordering between the keys and columns are inconsistent,
# the stops on this line go from right to left
if (next_key < stop_key) != (next_stop.col < stop.col):
return True
stop_key = next_key
stop = next_stop
return False
def move_to_correct_column(stop: Stop):
actions.edit.line_end()
move_cursor_left(stop.columns_left)
def move_to_correct_row(current_stop: Stop, next_stop: Stop):
start = current_stop.row
end = next_stop.row
if start < end:
for _ in range(end - start):
actions.edit.down()
elif start > end:
for _ in range(start - end):
actions.edit.up()
def format_tabs(text: str) -> str:
"""Possibly replaces tabs with spaces in the given text."""
spaces_per_tab: int = settings.get("user.snippet_raw_text_spaces_per_tab")
if spaces_per_tab < 0:
return text
return re.sub(r"\t", " " * spaces_per_tab, text)
def parse_snippet(body: str):
# Some IM services will send the message on a tab
body = format_tabs(body)
# Replace variable with appropriate value/text
body = re.sub(r"\$TM_SELECTED_TEXT", lambda _: actions.edit.selected_text(), body)
body = re.sub(r"\$CLIPBOARD", lambda _: actions.clip.text(), body)
lines = body.splitlines()
stops: list[Stop] = []
for i, line in enumerate(lines):
match = RE_STOP.search(line)
while match:
stops.append(
Stop(
name=match.group(1) or match.group(2) or match.group(3),
rows_up=len(lines) - i - 1,
columns_left=0,
row=i,
col=match.start(),
)
)
# Remove tab stops and variables.
stop_text = match.group(0)
default_value = match.group(4) or ""
line = line.replace(stop_text, default_value, 1)
# Might have multiple stops on the same line
match = RE_STOP.search(line)
# Update existing line
lines[i] = line
# Can't calculate column left until line text is fully updated
for stop in stops:
stop.columns_left = len(lines[stop.row]) - stop.col
updated_snippet = "\n".join(lines)
return updated_snippet, stops
def up(n: int):
"""Move cursor up <n> rows"""
for _ in range(n):
actions.edit.up()
def move_cursor_left(n: int):
"""Move cursor left <n> columns"""
for _ in range(n):
actions.edit.left()
def get_first_stop(stops: list[Stop]):
if not stops:
return None
stop = stops[0]
if stop.rows_up == 0 and stop.columns_left == 0:
return None
return stop
+451
View File
@@ -0,0 +1,451 @@
import logging
import re
from copy import deepcopy
from pathlib import Path
from typing import Callable, Union
from .snippet_types import Snippet, SnippetVariable
class SnippetDocument:
file: str
line_doc: int
line_body: int
variables: list[SnippetVariable] = []
name: str | None = None
description: str | None = None
phrases: list[str] | None = None
insertionScopes: list[str] | None = None
languages: list[str] | None = None
body: str | None = None
def __init__(self, file: str, line_doc: int, line_body: int):
self.file = file
self.line_doc = line_doc
self.line_body = line_body
def create_snippets_from_file(file: Path) -> list[Snippet]:
documents = parse_file(file)
return create_snippets(documents)
def create_snippets(documents: list[SnippetDocument]) -> list[Snippet]:
if len(documents) == 0:
return []
if documents[0].body is None:
default_context = documents[0]
documents = documents[1:]
else:
default_context = SnippetDocument("", -1, -1)
snippets: list[Snippet] = []
for doc in documents:
snippet = create_snippet(doc, default_context)
if snippet:
snippets.append(snippet)
return snippets
def create_snippet(
document: SnippetDocument,
default_context: SnippetDocument,
) -> Snippet | None:
body = normalize_snippet_body_tabs(document.body)
variables = combine_variables(default_context.variables, document.variables)
body, variables = add_final_stop_to_snippet_body(body, variables)
snippet = Snippet(
name=document.name or default_context.name or "",
description=document.description or default_context.description,
languages=document.languages or default_context.languages,
phrases=document.phrases or default_context.phrases,
insertion_scopes=document.insertionScopes or default_context.insertionScopes,
variables=variables,
body=body,
)
if not validate_snippet(document, snippet):
return None
return snippet
def validate_snippet(document: SnippetDocument, snippet: Snippet) -> bool:
is_valid = True
if not snippet.name:
error(document.file, document.line_doc, "Missing snippet name")
is_valid = False
if snippet.variables is None:
error(document.file, document.line_doc, "Missing snippet variables")
return False
for variable in snippet.variables:
var_name = f"${variable.name}"
if not is_variable_in_body(variable.name, snippet.body):
error(
document.file,
document.line_body,
f"Variable '{var_name}' missing in body '{snippet.body}'",
)
is_valid = False
if variable.insertion_formatters is not None and snippet.phrases is None:
error(
document.file,
document.line_doc,
f"Snippet phrase required when using '{var_name}.insertionFormatter'",
)
is_valid = False
if variable.wrapper_scope is not None and variable.wrapper_phrases is None:
error(
document.file,
document.line_doc,
f"'{var_name}.wrapperPhrase' required when using '{var_name}.wrapperScope'",
)
is_valid = False
return is_valid
def is_variable_in_body(variable_name: str, body: str) -> bool:
return (
re.search(create_variable_regular_expression(variable_name), body) is not None
)
def create_variable_regular_expression(variable_name: str) -> str:
# $value or ${value} or ${value:default}
# *? is used to find the smallest possible match.
# This stops multiple stops from being treated as a single stop.
return rf"\${variable_name}|\${{{variable_name}.*?}}"
def combine_variables(
default_variables: list[SnippetVariable],
document_variables: list[SnippetVariable],
) -> list[SnippetVariable]:
variables: dict[str, SnippetVariable] = {}
for variable in [*default_variables, *document_variables]:
if variable.name not in variables:
variables[variable.name] = SnippetVariable(variable.name)
new_variable = variables[variable.name]
if variable.insertion_formatters is not None:
new_variable.insertion_formatters = variable.insertion_formatters
if variable.wrapper_phrases is not None:
new_variable.wrapper_phrases = variable.wrapper_phrases
if variable.wrapper_scope is not None:
new_variable.wrapper_scope = variable.wrapper_scope
return list(variables.values())
def add_final_stop_to_snippet_body(
body: str, variables: list[SnippetVariable]
) -> tuple[str, list[SnippetVariable]]:
"""Make the snippet body end with stop $0 to allow exiting the snippet with `snip next`.
If the snippet has a stop named `0`, it will get replaced with the largest number of a snippet variable name
plus 1 with the original variable metadata for stop `0` now associated with the replacement.
"""
if body:
final_stop_matches = find_variable_matches("0", body)
# Only make a change if the snippet body does not end with a final stop.
if not (
len(final_stop_matches) > 0 and final_stop_matches[-1].end() == len(body)
):
biggest_variable_number: int | None = find_largest_variable_number(body)
if biggest_variable_number is not None:
replacement_name = str(biggest_variable_number + 1)
body = replace_final_stop(body, replacement_name, final_stop_matches)
variables = replace_variables_for_final_stop(
variables, replacement_name
)
body += "$0"
return body, variables
def replace_final_stop(body: str, replacement_name: str, final_stop_matches) -> str:
# Dealing with matches in reverse means replacing a match
# does not change the location of the remaining matches.
for match in reversed(final_stop_matches):
replacement = match.group().replace("0", replacement_name, 1)
body = body[: match.start()] + replacement + body[match.end() :]
return body
def replace_variables_for_final_stop(variables, replacement_name: str):
variables_clone = deepcopy(variables)
for variable in variables_clone:
if variable.name == "0":
variable.name = replacement_name
return variables_clone
def find_variable_matches(variable_name: str, body: str) -> list[re.Match[str]]:
"""Find every match of a variable in the body"""
expression = create_variable_regular_expression(variable_name)
matches = [m for m in re.finditer(expression, body)]
return matches
def find_largest_variable_number(body: str) -> int | None:
# Find all snippet stops with a numeric variable name
# +? is used to find the smallest possible match.
# We need this here to avoid treating multiple stops as a single one
regular_expression = rf"\$\d+?|\${{\d+?:.*?}}|\${{\d+?}}"
matches = re.findall(regular_expression, body)
if matches:
numbers = [
compute_first_integer_in_string(match)
for match in matches
if match is not None
]
if numbers:
return max(numbers)
return None
def compute_first_integer_in_string(text: str) -> int | None:
start_index: int | None = None
ending_index: int | None = None
for i, char in enumerate(text):
if char.isdigit():
if start_index is None:
start_index = i
ending_index = i + 1
elif start_index is not None:
break
if start_index is not None:
integer_text = text[start_index:ending_index]
return int(integer_text)
return None
def normalize_snippet_body_tabs(body: str | None) -> str:
if not body:
return ""
# If snippet body already contains tabs. No change.
if "\t" in body:
return body
lines = []
smallest_indentation = None
for line in body.splitlines():
match = re.search(r"^\s+", line)
indentation = match.group() if match is not None else ""
# Keep track of smallest non-empty indentation
if len(indentation) > 0 and (
smallest_indentation is None or len(indentation) < len(smallest_indentation)
):
smallest_indentation = indentation
lines.append({"indentation": indentation, "rest": line[len(indentation) :]})
# No indentation found in snippet body. No change.
if smallest_indentation is None:
return body
normalized_lines = [
reconstruct_line(smallest_indentation, line["indentation"], line["rest"])
for line in lines
]
return "\n".join(normalized_lines)
def reconstruct_line(smallest_indentation: str, indentation: str, rest: str) -> str:
# Update indentation by replacing each occurrent of smallest space indentation with a tab
indentation = indentation.replace(smallest_indentation, "\t")
return f"{indentation}{rest}"
# ---------- Snippet file parser ----------
def parse_file(file: Path) -> list[SnippetDocument]:
with open(file, encoding="utf-8") as f:
content = f.read()
return parse_file_content(file.name, content)
def parse_file_content(file: str, text: str) -> list[SnippetDocument]:
doc_texts = re.split(r"^---\n?$", text, flags=re.MULTILINE)
documents: list[SnippetDocument] = []
line = 0
for i, doc_text in enumerate(doc_texts):
optional_body = i == 0 and len(doc_texts) > 1
document = parse_document(file, line, optional_body, doc_text)
if document is not None:
documents.append(document)
line += doc_text.count("\n") + 1
return documents
def parse_document(
file: str,
line: int,
optional_body: bool,
text: str,
) -> Union[SnippetDocument, None]:
parts = re.split(r"^-$", text, maxsplit=1, flags=re.MULTILINE)
line_body = line + parts[0].count("\n") + 1
org_doc = SnippetDocument(file, line, line_body)
document = parse_context(file, line, org_doc, parts[0])
if len(parts) == 2:
body = parse_body(parts[1])
if body is not None:
if document is None:
document = org_doc
document.body = body
if document and not document.body and not optional_body:
error(file, line, f"Missing body in snippet document '{text}'")
return None
return document
def parse_context(
file: str,
line: int,
document: SnippetDocument,
text: str,
) -> Union[SnippetDocument, None]:
lines = [l.strip() for l in text.splitlines()]
keys: set[str] = set()
variables: dict[str, SnippetVariable] = {}
def get_variable(name: str) -> SnippetVariable:
if name not in variables:
variables[name] = SnippetVariable(name)
return variables[name]
for i, line_text in enumerate(lines):
if line_text:
parse_context_line(
file,
line + i,
document,
keys,
get_variable,
line_text,
)
if len(keys) == 0:
return None
document.variables = list(variables.values())
return document
def parse_context_line(
file: str,
line: int,
document: SnippetDocument,
keys: set[str],
get_variable: Callable[[str], SnippetVariable],
text: str,
):
parts = text.split(":")
if len(parts) != 2:
error(file, line, f"Invalid line '{text}'")
return
key = parts[0].strip()
value = parts[1].strip()
if not key or not value:
error(file, line, f"Invalid line '{text}'")
return
if key in keys:
warn(file, line, f"Duplicate key '{key}'")
keys.add(key)
match key:
case "name":
document.name = value
case "description":
document.description = value
case "phrase":
document.phrases = parse_vector_value(value)
case "insertionScope":
document.insertionScopes = parse_vector_value(value)
case "language":
document.languages = parse_vector_value(value)
case _:
if key.startswith("$"):
parse_variable(file, line, get_variable, key, value)
else:
warn(file, line, f"Unknown key '{key}'")
def parse_variable(
file: str,
line_numb: int,
get_variable: Callable[[str], SnippetVariable],
key: str,
value: str,
):
parts = key.split(".")
if len(parts) != 2:
error(file, line_numb, f"Invalid variable key '{key}'")
return
name = parts[0][1:]
field = parts[1]
match field:
case "insertionFormatter":
get_variable(name).insertion_formatters = parse_vector_value(value)
case "wrapperPhrase":
get_variable(name).wrapper_phrases = parse_vector_value(value)
case "wrapperScope":
get_variable(name).wrapper_scope = value
case _:
warn(file, line_numb, f"Unknown variable key '{key}'")
def parse_body(text: str) -> Union[str, None]:
# Find first line that is not empty. Preserve indentation.
match_leading = re.search(r"^[ \t]*\S", text, flags=re.MULTILINE)
if match_leading is None:
return None
return text[match_leading.start() :].rstrip()
def parse_vector_value(value: str) -> list[str]:
return [v.strip() for v in value.split("|")]
def error(file: str, line: int, message: str):
logging.error(f"{file}:{line+1} | {message}")
def warn(file: str, line: int, message: str):
logging.warning(f"{file}:{line+1} | {message}")