Skip to content

Commit ebf48bb

Browse files
committed
WIP
1 parent 01233bb commit ebf48bb

File tree

7 files changed

+374
-5
lines changed

7 files changed

+374
-5
lines changed

editors/vscode/package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,38 @@
4646
"type": "string",
4747
"default": "",
4848
"description": "Specifies an absolute path to the directory used to store samples and module data. Used only for local YARI. (example: /home/user/samples)"
49+
},
50+
"yls.snippets.metaEntries": {
51+
"type": "object",
52+
"default": {},
53+
"description": "A set of metadata entries to insert into rules. Empty values will create meta keys with a tabstop, and built-in variables are accepted (though transforms are ignored).",
54+
"maxProperties": 50,
55+
"additionalProperties": true
56+
},
57+
"yls.snippets.sortMeta": {
58+
"type": "boolean",
59+
"default": true,
60+
"description": "Sort the metadata entries in alphabetical order by keys. Otherwise, insert keys as listed."
61+
},
62+
"yls.snippets.condition": {
63+
"type": "boolean",
64+
"default": true,
65+
"description": "Enable the condition snippet on YARA rules. Has no effect on the presence of the condition section in the rule snippet."
66+
},
67+
"yls.snippets.meta": {
68+
"type": "boolean",
69+
"default": true,
70+
"description": "Enable the meta snippet on YARA rules. Has no effect on the presence of the meta section in the rule snippet."
71+
},
72+
"yls.snippets.rule": {
73+
"type": "boolean",
74+
"default": true,
75+
"description": "Enable the rule skeleton snippet on YARA rules. Settings for other snippets do not affect the sections provided in this snippet."
76+
},
77+
"yls.snippets.strings": {
78+
"type": "boolean",
79+
"default": true,
80+
"description": "Enable the strings snippet on YARA rules. Has no effect on the presence of the strings section in the rule snippet."
4981
}
5082
}
5183
},

tests/unit/test_snippet_string.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# type: ignore
2+
3+
from yls.snippet_string import SnippetString
4+
5+
6+
def test_simple():
7+
snip = SnippetString("test")
8+
assert str(snip) == "test"
9+
10+
11+
def test_placeholder():
12+
snip = SnippetString()
13+
snip.append_placeholder("one")
14+
snip.append_placeholder("two")
15+
assert str(snip) == "${1:one}${2:two}"
16+
17+
18+
def test_complex():
19+
snip = SnippetString()
20+
snip.append_text("text\n")
21+
snip.append_placeholder("one")
22+
snip.append_tabstop()
23+
snip.append_placeholder("two")
24+
snip.append_variable("CLIPBOARD", "")
25+
snip.append_variable("INVALID_VARIABLE", "default")
26+
snip.append_choice(("c", "ch", "choice"))
27+
assert str(snip) == "text\n${1:one}$2${3:two}${CLIPBOARD}${default}${4|c,ch,choice|}"

tests/unit/test_snippets.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# type: ignore
2+
3+
import pytest
4+
5+
from yls.snippets import SnippetGenerator
6+
7+
8+
@pytest.mark.parametrize(
9+
"config, expected",
10+
(
11+
(
12+
None,
13+
"""rule ${1:my_rule} {
14+
\tmeta:
15+
\t\t${2:KEY} = ${3:"VALUE"}
16+
\tstrings:
17+
\t\t${4:\\$name} = ${5|"string",/regex/,{ HEX }|}
18+
\tcondition:
19+
\t\t${6:any of them}
20+
}
21+
""",
22+
),
23+
(
24+
{},
25+
"""rule ${1:my_rule} {
26+
\tmeta:
27+
\t\t${2:KEY} = ${3:"VALUE"}
28+
\tstrings:
29+
\t\t${4:\\$name} = ${5|"string",/regex/,{ HEX }|}
30+
\tcondition:
31+
\t\t${6:any of them}
32+
}
33+
""",
34+
),
35+
(
36+
{"metaEntries": {"author": "test user", "hash": ""}},
37+
"""rule ${1:my_rule} {
38+
\tmeta:
39+
\t\tauthor = "test user"
40+
\t\thash = "$2"
41+
\tstrings:
42+
\t\t${3:\\$name} = ${4|"string",/regex/,{ HEX }|}
43+
\tcondition:
44+
\t\t${5:any of them}
45+
}
46+
""",
47+
),
48+
(
49+
{"metaEntries": {"filename": "${TM_FILENAME}"}},
50+
"""rule ${1:my_rule} {
51+
\tmeta:
52+
\t\tfilename = "${TM_FILENAME}"
53+
\tstrings:
54+
\t\t${2:\\$name} = ${3|"string",/regex/,{ HEX }|}
55+
\tcondition:
56+
\t\t${4:any of them}
57+
}
58+
""",
59+
),
60+
(
61+
{
62+
"metaEntries": {
63+
"author": "",
64+
"date": "${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}",
65+
}
66+
},
67+
"""rule ${1:my_rule} {
68+
\tmeta:
69+
\t\tauthor = "$2"
70+
\t\tdate = "${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}"
71+
\tstrings:
72+
\t\t${3:\\$name} = ${4|"string",/regex/,{ HEX }|}
73+
\tcondition:
74+
\t\t${5:any of them}
75+
}
76+
""",
77+
),
78+
(
79+
{
80+
"metaEntries": {
81+
"date": "${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}",
82+
"author": "",
83+
},
84+
"sortMeta": True,
85+
},
86+
"""rule ${1:my_rule} {
87+
\tmeta:
88+
\t\tauthor = "$2"
89+
\t\tdate = "${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}"
90+
\tstrings:
91+
\t\t${3:\\$name} = ${4|"string",/regex/,{ HEX }|}
92+
\tcondition:
93+
\t\t${5:any of them}
94+
}
95+
""",
96+
),
97+
),
98+
)
99+
def test_basic(config, expected):
100+
generator = SnippetGenerator(config)
101+
102+
assert expected == generator.generate()

yls/completer.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from yls.plugin_manager_provider import PluginManagerProvider
1414
from yls.strings import estimate_string_type
1515
from yls.strings import string_modifiers_completion_items
16+
from yls.snippets import SnippetGenerator
1617

1718
log = logging.getLogger(__name__)
1819

@@ -26,8 +27,9 @@ def __init__(self, ls: Any):
2627
self.ls = ls
2728
self.completion_cache = completion.CompletionCache.from_yaramod(self.ls.ymod)
2829

29-
def complete(self, params: lsp_types.CompletionParams) -> lsp_types.CompletionList:
30-
return lsp_types.CompletionList(is_incomplete=False, items=self._complete(params))
30+
async def complete(self, params: lsp_types.CompletionParams) -> lsp_types.CompletionList:
31+
items = await self._complete(params)
32+
return lsp_types.CompletionList(is_incomplete=False, items=items)
3133

3234
def signature_help(self, params: lsp_types.CompletionParams) -> lsp_types.SignatureHelp | None:
3335
signatures = self._signature_help(params)
@@ -71,7 +73,7 @@ def _signature_help(
7173

7274
return info
7375

74-
def _complete(self, params: lsp_types.CompletionParams) -> list[lsp_types.CompletionItem]:
76+
async def _complete(self, params: lsp_types.CompletionParams) -> list[lsp_types.CompletionItem]:
7577
document = self.ls.workspace.get_document(params.text_document.uri)
7678

7779
res = []
@@ -100,6 +102,10 @@ def _complete(self, params: lsp_types.CompletionParams) -> list[lsp_types.Comple
100102
log.debug("[COMPLETION] Adding last valid yara file")
101103
res += self.complete_last_valid_yara_file(document, params, word)
102104

105+
# Dynamic snippets
106+
log.debug("[COMPLETION] Adding dynamic snippets")
107+
res += await self.complete_dynamic_snippets()
108+
103109
# Plugin completion
104110
log.debug("COMPLETION] Adding completion items from plugings")
105111
res += utils.flatten_list(
@@ -238,3 +244,12 @@ def complete_condition_keywords(
238244

239245
res.append(item)
240246
return res
247+
248+
async def complete_dynamic_snippets(self) -> list[lsp_types.CompletionItem]:
249+
config = await utils.get_config_from_editor(self.ls, "yls.snippets")
250+
log.debug(f"[COMPLETION] lsp configuration {config=}")
251+
if config is None:
252+
return []
253+
254+
generator = SnippetGenerator(config)
255+
return generator.generate_snippets()

yls/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,13 @@ def initiliazed(ls: YaraLanguageServer, _params: Any) -> None:
226226

227227

228228
@SERVER.feature(COMPLETION, lsp_types.CompletionOptions(trigger_characters=["."]))
229-
def completion(
229+
async def completion(
230230
ls: YaraLanguageServer, params: lsp_types.CompletionParams
231231
) -> lsp_types.CompletionList:
232232
"""Code completion."""
233233
utils.log_command(COMPLETION)
234234

235-
return ls.completer.complete(params)
235+
return await ls.completer.complete(params)
236236

237237

238238
@SERVER.feature(SIGNATURE_HELP, lsp_types.SignatureHelpOptions(trigger_characters=["("]))

yls/snippet_string.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from __future__ import annotations
2+
from typing import Iterable
3+
4+
SUPPORTED_VARIABLES = {
5+
"TM_SELECTED_TEXT",
6+
"TM_CURRENT_LINE",
7+
"TM_CURRENT_WORD",
8+
"TM_LINE_INDEX",
9+
"TM_LINE_NUMBER",
10+
"TM_FILENAME",
11+
"TM_FILENAME_BASE",
12+
"TM_DIRECTORY",
13+
"TM_FILEPATH",
14+
"RELATIVE_FILEPATH",
15+
"CLIPBOARD",
16+
"WORKSPACE_NAME",
17+
"WORKSPACE_FOLDER",
18+
"CURSOR_INDEX",
19+
"CURSOR_NUMBER",
20+
"CURRENT_YEAR",
21+
"CURRENT_YEAR_SHORT",
22+
"CURRENT_MONTH",
23+
"CURRENT_MONTH_NAME",
24+
"CURRENT_MONTH_NAME_SHORT",
25+
"CURRENT_DATE",
26+
"CURRENT_DAY_NAME",
27+
"CURRENT_DAY_NAME_SHORT",
28+
"CURRENT_HOUR",
29+
"CURRENT_MINUTE",
30+
"CURRENT_SECOND",
31+
"CURRENT_SECONDS_UNIX",
32+
"RANDOM",
33+
"RANDOM_HEX",
34+
"UUID",
35+
"BLOCK_COMMENT_START",
36+
"BLOCK_COMMENT_END",
37+
"LINE_COMMENT",
38+
}
39+
40+
41+
class SnippetString:
42+
cur_idx: int
43+
value: str
44+
45+
def __init__(self, value: str = ""):
46+
self.value = value
47+
self.cur_idx = 1
48+
49+
def append_choice(self, values: Iterable[str]) -> None:
50+
self.value += f"${{{self.get_and_inc()}|{','.join(values)}|}}"
51+
52+
def append_placeholder(self, value: str) -> None:
53+
self.value += f"${{{self.get_and_inc()}:{value}}}"
54+
55+
def append_tabstop(self) -> None:
56+
self.value += f"${self.get_and_inc()}"
57+
58+
def append_text(self, value: str) -> None:
59+
"""WARNING: For now you are expected to escape the string if necessary."""
60+
self.value += value
61+
62+
def append_variable(self, name: str, default_value: str) -> None:
63+
if name in SUPPORTED_VARIABLES:
64+
self.value += f"${{{name}}}"
65+
else:
66+
self.value += f"${{{default_value}}}"
67+
68+
def get_and_inc(self) -> int:
69+
i = self.cur_idx
70+
self.cur_idx += 1
71+
return i
72+
73+
def __str__(self) -> str:
74+
return self.value

0 commit comments

Comments
 (0)