Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 0.23.0 #98

Merged
merged 3 commits into from
Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Note: If you'd like to customize any of the spoken forms, please see the [docume
- [Scroll](#scroll)
- [Insert/Use/Repeat](#insertuserepeat)
- [Wrap](#wrap)
- [\[experimental\] Wrap with snippet](#experimental-wrap-with-snippet)
- [Show definition/reference/quick fix](#show-definitionreferencequick-fix)
- [Fold/unfold](#foldunfold)
- [Extract](#extract)
Expand Down Expand Up @@ -382,6 +383,10 @@ eg:
`square wrap blue air`
Wraps the token containing letter 'a' with a blue hat in square brackets.

#### \[experimental\] Wrap with snippet

See [experimental documentation](experimental.md#wrapper-snippets).

### Show definition/reference/quick fix

- `"def show"`
Expand Down
97 changes: 97 additions & 0 deletions docs/experimental.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Experimental features

Here we document features which are currently considered experimental. They are generally functional and well tested, but the API is subject change.

## Wrapper snippets

![Wrapper snippet demo](images/tryWrapFine.gif)

In addition to wrapping with paired delimiters (eg `"square wrap"`, `"round wrap"`, etc), we experimentally support wrapping with snippets. Cursorless ships with a few built-in snippets, but users can also use their own snippets.

### Enabling wrapper snippets

Add the following line to the end of your `settings.talon` (or any other `.talon` file that will be active when vscode is focused):

```
tag(): user.cursorless_experimental_snippets
```

### Using wrapper snippets

#### Command syntax

The command syntax is as follows:

```
"<snippet_name> wrap <target>"
```

#### Examples

- `"try wrap air"`: Wrap the statement containing the marked `a` in a try-catch statement
- `"try wrap air past bat"`: Wrap the sequence of statements from the marked `a` to the marked `b` in a try-catch statement

#### Default scope types

Each snippet wrapper has a default scope type. When you refer to a target, by default it will expand to the given scope type. This way, for example, when you say `"try wrap air"`, it will refer to the statement containing `a` rather than just the token.

### Built-in wrapper snippets

| Default spoken form | Snippet | Default target scope type |
| ------------------- | --------------------------------------------- | ------------------------- |
| `"if wrap"` | If statement | Statement |
| `"else wrap"` | If-else statement; target goes in else branch | Statement |
| `"if else wrap"` | If-else statement; target goes in if branch | Statement |
| `"try wrap"` | Try-catch statement | Statement |

### Customizing spoken forms

As usual, the spoken forms for these wrapper snippets can be [customized by csv](customization.md). The csvs are in the file `cursorless-settings/experimental/wrapper_snippets.csv`.

### Adding your own snippets

To define your own wrapper snippets, proceed as follows:

#### Define snippets in vscode

1. Set the `cursorless.experimental.snippetsDir` setting to a directory in which you'd like to create your snippets.
2. Add snippets to the directory in files ending in `.cursorless-snippets`. See the [documentation](https://github.com/pokey/cursorless-vscode/tree/main/docs/experimental/snippets.md) for the cursorless snippet format.

#### 2. Add snippet to spoken forms csv

For each snippet that you'd like to be able to use as a wrapper snippet, add a line to the `cursorless-settings/experimental/wrapper_snippets.csv` csv overrides file. The first column is the desired spoken form, and the second column is of the form `<name>.<variable>`, where `name` is the name of the snippet (ie the key in your snippet json file), and `variable` is one of the placeholder variables in your snippet where the target should go.

### Customizing built-in snippets

To customize a built-in snippet, just define a custom snippet (as above), but
use the same name as the cursorless core snippet you'd like to change, and give
definitions along with scopes where you'd like your override to be active. Here
is an example:

```json
{
"tryCatchStatement": {
"definitions": [
{
"scope": {
"langIds": [
"typescript",
"typescriptreact",
"javascript",
"javascriptreact"
]
},
"body": [
"try {",
"\t$body",
"} catch (err) {",
"\t$exceptBody",
"}"
]
}
]
}
}
```

The above will change the definition of the try-catch statement in typescript.
Binary file added docs/images/tryWrapFine.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 64 additions & 5 deletions src/actions/wrap.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,73 @@
from typing import Union
from ..paired_delimiter import paired_delimiters_map
from talon import Module
from talon import Module, actions, app, Context
from ..csv_overrides import init_csv_and_watch_changes


mod = Module()

mod.tag(
"cursorless_experimental_snippets",
desc="tag for enabling experimental snippet support",
)

mod.list("cursorless_wrap_action", desc="Cursorless wrap action")
mod.list("cursorless_wrapper_snippet", desc="Cursorless wrapper snippet")

experimental_snippets_ctx = Context()
experimental_snippets_ctx.matches = r"""
tag: user.cursorless_experimental_snippets
"""

@mod.capture(rule=("{user.cursorless_paired_delimiter}"))
def cursorless_wrapper(m) -> list[str]:
paired_delimiter_info = paired_delimiters_map[m.cursorless_paired_delimiter]
return [paired_delimiter_info.left, paired_delimiter_info.right]

# NOTE: Please do not change these dicts. Use the CSVs for customization.
# See https://github.com/pokey/cursorless-talon/blob/main/docs/customization.md
wrapper_snippets = {
"else": "ifElseStatement.alternative",
"if else": "ifElseStatement.consequence",
"if": "ifStatement.consequence",
"try": "tryCatchStatement.body",
}


@mod.capture(
rule=(
"({user.cursorless_paired_delimiter} | {user.cursorless_wrapper_snippet}) {user.cursorless_wrap_action}"
)
)
def cursorless_wrapper(m) -> Union[list[str], str]:
try:
paired_delimiter_info = paired_delimiters_map[m.cursorless_paired_delimiter]
return {
"action": "wrapWithPairedDelimiter",
"extra_args": [paired_delimiter_info.left, paired_delimiter_info.right],
}
except AttributeError:
return {
"action": "wrapWithSnippet",
"extra_args": [m.cursorless_wrapper_snippet],
}


@mod.action_class
class Actions:
def cursorless_wrap(cursorless_wrapper: dict, targets: dict):
"""Perform cursorless wrap action"""
actions.user.cursorless_single_target_command_with_arg_list(
cursorless_wrapper["action"], targets, cursorless_wrapper["extra_args"]
)


def on_ready():
init_csv_and_watch_changes(
"experimental/wrapper_snippets",
{
"wrapper_snippet": wrapper_snippets,
},
allow_unknown_values=True,
default_list_name="wrapper_snippet",
ctx=experimental_snippets_ctx,
)


app.register("ready", on_ready)
102 changes: 78 additions & 24 deletions src/csv_overrides.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import Optional
from .conventions import get_cursorless_list_name
from talon import Context, Module, actions, fs, app, settings
from datetime import datetime
from pathlib import Path


mod = Module()
ctx = Context()
cursorless_settings_directory = mod.setting(
"cursorless_settings_directory",
type=str,
Expand All @@ -17,7 +17,10 @@
def init_csv_and_watch_changes(
filename: str,
default_values: dict[str, dict],
extra_acceptable_values: list[str] = None,
extra_ignored_values: list[str] = None,
allow_unknown_values: bool = False,
default_list_name: Optional[str] = None,
ctx: Context = Context(),
):
"""
Initialize a cursorless settings csv, creating it if necessary, and watch
Expand All @@ -37,37 +40,67 @@ def init_csv_and_watch_changes(
`cursorles-settings` dir
default_values (dict[str, dict]): The default values for the lists to
be customized in the given csv
extra_acceptable_values list[str]: Don't throw an exception if any of
these appear as values
extra_ignored_values list[str]: Don't throw an exception if any of
these appear as values; just ignore them and don't add them to any list
allow_unknown_values bool: If unknown values appear, just put them in the list
default_list_name Optional[str]: If unknown values are allowed, put any
unknown values in this list
"""
if extra_acceptable_values is None:
extra_acceptable_values = []
if extra_ignored_values is None:
extra_ignored_values = []

dir_path, file_path = get_file_paths(filename)
file_path = get_full_path(filename)
super_default_values = get_super_values(default_values)

dir_path.mkdir(parents=True, exist_ok=True)
file_path.parent.mkdir(parents=True, exist_ok=True)

def on_watch(path, flags):
if file_path.match(path):
current_values, has_errors = read_file(
file_path, super_default_values.values(), extra_acceptable_values
file_path,
super_default_values.values(),
extra_ignored_values,
allow_unknown_values,
)
update_dicts(
default_values,
current_values,
extra_ignored_values,
allow_unknown_values,
default_list_name,
ctx,
)
update_dicts(default_values, current_values, extra_acceptable_values)

fs.watch(dir_path, on_watch)
fs.watch(file_path.parent, on_watch)

if file_path.is_file():
current_values = update_file(
file_path, super_default_values, extra_acceptable_values
file_path,
super_default_values,
extra_ignored_values,
allow_unknown_values,
)
update_dicts(
default_values,
current_values,
extra_ignored_values,
allow_unknown_values,
default_list_name,
ctx,
)
update_dicts(default_values, current_values, extra_acceptable_values)
else:
create_file(file_path, super_default_values)
update_dicts(default_values, super_default_values, extra_acceptable_values)
update_dicts(
default_values,
super_default_values,
extra_ignored_values,
allow_unknown_values,
default_list_name,
ctx,
)

def unsubscribe():
fs.unwatch(dir_path, on_watch)
fs.unwatch(file_path.parent, on_watch)

return unsubscribe

Expand All @@ -79,7 +112,10 @@ def is_removed(value: str):
def update_dicts(
default_values: dict[str, dict],
current_values: dict,
extra_acceptable_values: list[str],
extra_ignored_values: list[str],
allow_unknown_values: bool,
default_list_name: Optional[str],
ctx: Context,
):
# Create map with all default values
results_map = {}
Expand All @@ -92,8 +128,14 @@ def update_dicts(
try:
results_map[value]["key"] = key
except KeyError:
if value in extra_acceptable_values:
if value in extra_ignored_values:
pass
elif allow_unknown_values:
results_map[value] = {
"key": key,
"value": value,
"list": default_list_name,
}
else:
raise

Expand All @@ -110,9 +152,14 @@ def update_dicts(
ctx.lists[get_cursorless_list_name(list_name)] = dict


def update_file(path: Path, default_values: dict, extra_acceptable_values: list[str]):
def update_file(
path: Path,
default_values: dict,
extra_ignored_values: list[str],
allow_unknown_values: bool,
):
current_values, has_errors = read_file(
path, default_values.values(), extra_acceptable_values
path, default_values.values(), extra_ignored_values, allow_unknown_values
)
current_identifiers = current_values.values()

Expand Down Expand Up @@ -178,7 +225,10 @@ def csv_error(path: Path, index: int, message: str, value: str):


def read_file(
path: Path, default_identifiers: list[str], extra_acceptable_values: list[str]
path: Path,
default_identifiers: list[str],
extra_ignored_values: list[str],
allow_unknown_values: bool,
):
with open(path) as f:
lines = list(f)
Expand Down Expand Up @@ -210,7 +260,11 @@ def read_file(
seen_header = True
continue

if value not in default_identifiers and value not in extra_acceptable_values:
if (
value not in default_identifiers
and value not in extra_ignored_values
and not allow_unknown_values
):
has_errors = True
csv_error(path, i, "Unknown identifier", value)
continue
Expand All @@ -229,17 +283,17 @@ def read_file(
return result, has_errors


def get_file_paths(filename: str):
def get_full_path(filename: str):
if not filename.endswith(".csv"):
filename = f"{filename}.csv"

user_dir = actions.path.talon_user()
settings_directory = Path(cursorless_settings_directory.get())

if not settings_directory.is_absolute():
settings_directory = user_dir / settings_directory

csv_path = Path(settings_directory, filename)
return settings_directory, csv_path
return (settings_directory / filename).resolve()


def get_super_values(values: dict[str, dict]):
Expand Down
Loading