ZedScripts 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from lsprotocol import types
4
+
5
+ from ..utils import textrange_to_lsp
6
+
7
+ if TYPE_CHECKING:
8
+ from ..structure.lexer import TextRange
9
+
10
+ def make_hover_information(description: str, range: 'TextRange | None' = None) -> types.Hover | None:
11
+ if description == "":
12
+ return None
13
+ return types.Hover(
14
+ contents=types.MarkupContent(
15
+ kind=types.MarkupKind.Markdown,
16
+ value=_format_description(description),
17
+ ),
18
+ range=textrange_to_lsp(range) if range is not None else None
19
+ )
20
+
21
+ def format_tree(tree: str) -> str:
22
+ return f"```zedserver\n{tree}\n```"
23
+
24
+ # TODO: this could possibly not work in other IDEs
25
+ def _format_description(description: str) -> str:
26
+ """
27
+ Format the description for hover information.
28
+ The C++ code blocks which are used in the datasets for script blocks are
29
+ replaced with proper zedscripts code blocks.
30
+
31
+ Args:
32
+ description (str):
33
+
34
+ Returns:
35
+ str:
36
+ """
37
+ return description.replace("```cpp", "```zedscripts")
@@ -0,0 +1,49 @@
1
+ import datetime
2
+ import logging
3
+ import requests_cache
4
+
5
+ from .. import ZEDSCRIPT_CACHE_DIR
6
+
7
+
8
+
9
+
10
+ _default_expire_after = datetime.timedelta(hours=12)
11
+ def get_session(expire_after: datetime.timedelta | float = _default_expire_after) -> requests_cache.CachedSession:
12
+ """
13
+ Get a cached HTTP session with the specified expiration time.
14
+
15
+ Args:
16
+ expire_after (datetime.timedelta | float, optional): The duration after which the cache expires. Defaults to datetime.timedelta(days=1).
17
+
18
+ Returns:
19
+ requests_cache.CachedSession: A cached HTTP session with the specified expiration time.
20
+ """
21
+ session = requests_cache.CachedSession(
22
+ cache_name=str(ZEDSCRIPT_CACHE_DIR / "http_cache"),
23
+ expire_after=expire_after,
24
+ )
25
+ return session
26
+
27
+ def load_json(url: str, expire_after: datetime.timedelta | float = _default_expire_after) -> dict:
28
+ """
29
+ Load JSON data from the specified URL using a cached HTTP session.
30
+
31
+ Args:
32
+ url (str): The URL to load JSON data from.
33
+ expire_after (datetime.timedelta | float, optional): The duration after which the cache expires. Defaults to _default_expire_after.
34
+
35
+ Returns:
36
+ dict: The JSON data loaded from the specified URL.
37
+ """
38
+ session = get_session(expire_after=expire_after)
39
+ response = session.get(url)
40
+ is_cached = getattr(response, 'from_cache', False)
41
+ logging.debug(f"Loading {url} - from_cache: {is_cached}")
42
+ response.raise_for_status()
43
+ return response.json()
44
+
45
+ def clear_cache():
46
+ """Resets the session cache."""
47
+ session = get_session()
48
+ session.cache.clear()
49
+ logging.debug("HTTP cache cleared.")
@@ -0,0 +1,80 @@
1
+ import json
2
+ import logging
3
+ from typing import TYPE_CHECKING, Any
4
+ from importlib.resources import files
5
+
6
+ if TYPE_CHECKING:
7
+ from ..enums.Diagnostic import DiagnosticType
8
+ from ..providers.diagnostics import DiagnosticDefinition
9
+
10
+
11
+ class Locale:
12
+ def __init__(self, code: str, strings: dict[str, str] | None = None) -> None:
13
+ if strings is None:
14
+ strings = {}
15
+ self.code: str = code
16
+ self.strings: dict[str, str] = strings
17
+
18
+
19
+ class Localizer:
20
+ def __init__(self) -> None:
21
+ self.locales: dict[str, Locale] = {}
22
+ self.default_locale: str = "en"
23
+ self.current_locale: str = self.default_locale
24
+
25
+ def localize_string(self,
26
+ identifier: 'DiagnosticType',
27
+ definition: 'DiagnosticDefinition',
28
+ locale: str | None = None,
29
+ args: dict[str, Any] | None = None) -> str:
30
+ if locale is None:
31
+ locale = self.current_locale
32
+ assert locale in self.locales, "Locale not loaded or invalid: {}".format(locale)
33
+
34
+ string = self.locales[locale].strings.get(identifier.name)
35
+ if string is None:
36
+ logging.warning("Missing string: %s (%s).", identifier.name, locale)
37
+ return identifier.name
38
+
39
+ if args is not None:
40
+ args_formatted = {}
41
+ args_def = definition.args
42
+
43
+ # ensure that each keys in args_def are present in args
44
+ for k in args_def.keys():
45
+ if k not in args:
46
+ raise ValueError(f"Missing argument '{k}' for localization.")
47
+
48
+ # format args properly based on the definition's argument types
49
+ for k,v in args.items():
50
+ assert k in args_def.keys(), f"Unexpected argument '{k}' for localization."
51
+ assert isinstance(v, args_def[k]), f"Argument '{k}' must be of type {args_def[k].__name__}."
52
+
53
+ # format based on the type
54
+ if isinstance(args_def[k], list):
55
+ args_formatted[k] = ", ".join(f"'{item}'" for item in v)
56
+ else:
57
+ args_formatted[k] = str(v)
58
+ return string.format(**args_formatted)
59
+
60
+ return string
61
+
62
+ def load_locale_files(self) -> None:
63
+ for locale in files("ZedScripts.locale").iterdir():
64
+ # safeguards, probably not needed tbh
65
+ if not locale.is_file():
66
+ continue
67
+ if not locale.name.endswith(".json"):
68
+ continue
69
+
70
+ # access the locale data
71
+ with locale.open('r') as file:
72
+ raw: dict[str, Any] = json.load(file)
73
+
74
+ assert "code" in raw, "Locale file missing 'code'"
75
+ assert "strings" in raw, "Locale file missing 'strings'"
76
+
77
+ self.locales[raw["code"]] = Locale(raw["code"], raw["strings"])
78
+
79
+ # initialize the localizer here to prevent circular imports
80
+ zedlocalizer = Localizer()
@@ -0,0 +1,45 @@
1
+ import enum
2
+ from typing import TypedDict
3
+ from pathlib import Path
4
+
5
+ from ..utils import path_to_uri
6
+
7
+ from typing import TYPE_CHECKING
8
+ if TYPE_CHECKING:
9
+ from ..environment import WorkspaceType
10
+
11
+
12
+ class ZedNotification(enum.StrEnum):
13
+ SET_ZEDSCRIPTS = "zedscripts/setZedScripts"
14
+
15
+ LOADING_DOCUMENTS = "zedscripts/loadingDocuments"
16
+ LOADING_DOCUMENTS_DONE = "zedscripts/loadingDocumentsDone"
17
+
18
+
19
+
20
+ class NotificationParams(TypedDict): ...
21
+
22
+ class SetZedScriptsNotificationParams(NotificationParams):
23
+ uri: str
24
+
25
+ class LoadingDocumentsNotificationParams(NotificationParams):
26
+ uri: str
27
+ progress: float
28
+ workspace_type: 'WorkspaceType'
29
+
30
+
31
+
32
+
33
+ def send_progress_notification(type: ZedNotification, folder: Path, progress: float, workspace_type: 'WorkspaceType'):
34
+ from ..server import ZedServer
35
+ server = ZedServer.instance
36
+ if server is None:
37
+ return
38
+ server.send_notification(
39
+ type,
40
+ LoadingDocumentsNotificationParams(
41
+ uri=path_to_uri(folder),
42
+ progress=progress,
43
+ workspace_type=workspace_type
44
+ )
45
+ )
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ import re
5
+ import logging
6
+ from typing import TYPE_CHECKING, cast
7
+ from operator import attrgetter
8
+
9
+ from lsprotocol.types import SemanticTokens
10
+
11
+ from .. import IS_DEBUG
12
+ from ..structure.lexer import TokenType, chars_in_range, Token, TokenCollection, TextPosition, TextRange
13
+
14
+ if TYPE_CHECKING:
15
+ from ..environment.document import Document
16
+
17
+ PATTERN_FLOAT = re.compile("-?\\d+(?:\\.\\d+)?")
18
+
19
+
20
+ class SemanticTokenType(enum.IntEnum):
21
+ NAMESPACE = 0
22
+ TYPE = 1
23
+ CLASS = 2
24
+ ENUM = 3
25
+ INTERFACE = 4
26
+ STRUCT = 5
27
+ TYPE_PARAMETER = 6
28
+ PARAMETER = 7
29
+ VARIABLE = 8
30
+ PROPERTY = 9
31
+ ENUM_MEMBER = 10
32
+ EVENT = 11
33
+ FUNCTION = 12
34
+ METHOD = 13
35
+ MACRO = 14
36
+ KEYWORD = 15
37
+ MODIFIER = 16
38
+ COMMENT = 17
39
+ STRING = 18
40
+ NUMBER = 19
41
+ REGEXP = 20
42
+ OPERATOR = 21
43
+ DECORATOR = 22
44
+ LABEL = 23
45
+
46
+
47
+ class SemanticTokenModifier(enum.IntFlag):
48
+ DECLARATION = enum.auto()
49
+ """declaration site of a symbol"""
50
+ DEFINITION = enum.auto()
51
+ """definition (vs. just a declaration)"""
52
+ READONLY = enum.auto()
53
+ """symbol can't be reassigned (e.g. a constant)"""
54
+ STATIC = enum.auto()
55
+ """static member/variable"""
56
+ DEPRECATED = enum.auto()
57
+ """symbol is deprecated, editors often show strikethrough"""
58
+ ABSTRACT = enum.auto()
59
+ """abstract method/class"""
60
+ ASYNC = enum.auto()
61
+ """async function/method"""
62
+ MODIFICATION = enum.auto()
63
+ """token is a write/assignment to the symbol, not just a read"""
64
+ DOCUMENTATION = enum.auto()
65
+ """token appears inside documentation"""
66
+ DEFAULT_LIBRARY = enum.auto()
67
+ """symbol comes from a built-in/default library"""
68
+
69
+ def get_tokens() -> tuple[list[str], list[str]]:
70
+ """
71
+ Returns the list of semantic token types and modifiers as strings.
72
+ """
73
+ return [t.name.lower() for t in SemanticTokenType], [cast(str, m.name).lower() for m in SemanticTokenModifier]
74
+
75
+
76
+
77
+
78
+
79
+
80
+ class SemanticToken:
81
+ def __init__(
82
+ self, range: TextRange, type: SemanticTokenType, modifiers: SemanticTokenModifier | None = None
83
+ ) -> None:
84
+ if modifiers is None:
85
+ modifiers = SemanticTokenModifier(0)
86
+ self.range: TextRange = range
87
+ self.type: SemanticTokenType = type
88
+ self.modifiers: SemanticTokenModifier = modifiers
89
+
90
+ def __repr__(self) -> str:
91
+ return f"SemanticToken(range={self.range}, type={self.type.name}[{self.type}], modifiers={self.modifiers})"
92
+
93
+
94
+ class SemanticTokenCollection(list[SemanticToken]):
95
+ def __init__(self, document: Document, *args: SemanticToken) -> None:
96
+ super().__init__(args)
97
+ self.document = document
98
+
99
+ def sort_tokens(self) -> "SemanticTokenCollection":
100
+ self.sort(key=attrgetter("range.start.line", "range.start.offset"))
101
+ return self
102
+
103
+ def to_lsp(self) -> list[int]:
104
+ lsp_tokens: list[int] = []
105
+ last_pos: TextPosition = TextPosition(0, 0)
106
+
107
+ # make sure to sort the semantic tokens by their position
108
+ semantic_tokens = self.sort_tokens()
109
+ for i in range(len(semantic_tokens)):
110
+ token = semantic_tokens[i]
111
+
112
+ line_delta = token.range.start.line - last_pos.line
113
+ if line_delta == 0:
114
+ offset_delta = token.range.start.offset - last_pos.offset
115
+ else:
116
+ offset_delta = token.range.start.offset
117
+
118
+ characters = chars_in_range(self.document.get_text(), token.range)
119
+ if characters == "":
120
+ continue
121
+
122
+ lines = characters.split("\n")
123
+ lsp_tokens.append(line_delta)
124
+ lsp_tokens.append(offset_delta)
125
+ lsp_tokens.append(len(lines[0]))
126
+ lsp_tokens.append(token.type.value)
127
+ # false positive U_U
128
+ lsp_tokens.append(int(token.modifiers))
129
+ for line in lines[1:]:
130
+ lsp_tokens.append(1)
131
+ lsp_tokens.append(0)
132
+ lsp_tokens.append(len(line))
133
+ lsp_tokens.append(token.type.value)
134
+ # false positive U_U
135
+ lsp_tokens.append(int(token.modifiers))
136
+
137
+ last_pos = token.range.start if len(lines) == 1 else TextPosition(token.range.start.line + len(lines) - 1, 0)
138
+
139
+ # just so we don't have a for loop when not in debug mode
140
+ # if IS_DEBUG:
141
+ # logging.debug("Semantic tokens for document")
142
+ # max_token_length = 70
143
+ # for line, token in enumerate(semantic_tokens):
144
+ # logging.debug(f"{str(token).ljust(max_token_length+2)} {lsp_tokens[line * 5: (line + 1) * 5]}")
145
+
146
+ return lsp_tokens
147
+
148
+ def add(self, type: SemanticTokenType, location: TextRange, modifiers: list[SemanticTokenModifier] = []):
149
+ """
150
+ Add a new semantic token to the collection.
151
+
152
+ Args:
153
+ type (SemanticTokenType): The type of the semantic token.
154
+ location (TextRange): The range in the document that the token covers.
155
+ """
156
+ combined_modifiers = SemanticTokenModifier(0)
157
+ for modifier in modifiers:
158
+ combined_modifiers |= modifier
159
+ self.append(SemanticToken(location, type, combined_modifiers))
160
+
161
+
162
+ def build_syntactic_tokens(document: Document):
163
+ """
164
+ Builds tokens that can be inferred directly from the lexical tokens without further context.
165
+ :param document:
166
+ :return:
167
+ """
168
+ semantic_tokens = document.syntactic_semantic_tokens
169
+ semantic_tokens.clear() # reset syntactic semantic tokens
170
+
171
+ # mark every typical lexical token with a corresponding semantic token
172
+ for token in document.lexical_tokens:
173
+ match token.type:
174
+ case TokenType.ELEMENT_DELIMITER:
175
+ semantic_tokens.add(
176
+ SemanticTokenType.KEYWORD,
177
+ TextRange(token.pos, token.end),
178
+ )
179
+ case TokenType.COMMENT:
180
+ semantic_tokens.add(
181
+ SemanticTokenType.COMMENT,
182
+ TextRange(token.pos, token.end),
183
+ )
184
+ case TokenType.PUNCTUATOR:
185
+ semantic_tokens.add(
186
+ SemanticTokenType.KEYWORD,
187
+ TextRange(token.pos, token.end),
188
+ )
189
+
@@ -0,0 +1,160 @@
1
+ import enum
2
+ from typing import (TYPE_CHECKING,
3
+ TypedDict, Literal, NotRequired,
4
+ TypeVar, Generic)
5
+
6
+ if TYPE_CHECKING:
7
+ from ..structure.ast import ValueNode, BlockNode, Chunk
8
+
9
+
10
+
11
+ class ScriptBlockData(TypedDict):
12
+ """
13
+ The data structure representing a script block.
14
+ """
15
+ name: str
16
+ description: str
17
+ shortDescription: NotRequired[str]
18
+ needsChildren: NotRequired[list[str]]
19
+ parents: list[str]
20
+ ID: NotRequired['ScriptBlockID']
21
+ parameters: dict[str, 'ScriptBlockParameter']
22
+ # properties: #TODO
23
+ variantOf: NotRequired[str]
24
+ """if this block is a variant of another block, the name of the base block"""
25
+
26
+ # those should basically be unused here now
27
+ isRoot: NotRequired[bool]
28
+ pattern: NotRequired[list[str]]
29
+ """to be used as regex patterns for identification"""
30
+ noComma: NotRequired[bool]
31
+ """default is false"""
32
+
33
+ class ScriptBlockID(TypedDict):
34
+ """
35
+ Provides information about the ID of the block.
36
+ """
37
+ parentsWithout: NotRequired[list[str]]
38
+ values: NotRequired[list[str]]
39
+ asType: NotRequired[bool]
40
+ canHaveSpace: NotRequired[bool]
41
+ translation: NotRequired['TranslationProperties']
42
+
43
+
44
+ ## key-value types
45
+
46
+ type ScriptBlockValue = str | int | float | bool
47
+ class ScriptBlockParameter(TypedDict):
48
+ """
49
+ Represents a parameter of a script block.
50
+ """
51
+ name: str
52
+ description: NotRequired[str]
53
+ allowedDuplicate: NotRequired[bool]
54
+ canBeEmpty: NotRequired[bool]
55
+ default: NotRequired[ScriptBlockValue | list]
56
+ type: NotRequired['ParameterType']
57
+ deprecated: NotRequired['DeprecatedInfo']
58
+ values: NotRequired[list[ScriptBlockValue]]
59
+ needs: NotRequired['ScriptBlockNeeds']
60
+
61
+
62
+ class ValueType(enum.StrEnum):
63
+ STRING = "string"
64
+ INTEGER = "integer"
65
+ FLOAT = "float"
66
+ BOOLEAN = "boolean"
67
+ ARRAY = "array"
68
+ OBJECT = "object"
69
+ BLOCK = "block"
70
+ CALLBACK = "callback"
71
+ TRANSLATION = "translation"
72
+
73
+ _early_return: set[ValueType] = {
74
+ ValueType.STRING,
75
+ ValueType.ARRAY,
76
+ ValueType.OBJECT,
77
+ ValueType.BLOCK,
78
+ ValueType.CALLBACK,
79
+ ValueType.TRANSLATION,
80
+ }
81
+
82
+ def cant_self_validate(value_type: ValueType) -> bool:
83
+ """Verifies whenever the provided value_type cannot be validated by the value itself."""
84
+ return value_type in _early_return
85
+
86
+
87
+ class ParameterType(TypedDict):
88
+ main: ValueType
89
+ array: NotRequired['ArrayType']
90
+ object: NotRequired['ObjectType']
91
+ block: NotRequired['BlockType']
92
+ translation: NotRequired['TranslationProperties']
93
+ callback: NotRequired['CallbackType']
94
+
95
+ class ArrayType(TypedDict):
96
+ separator: str
97
+ type: Literal[ValueType.STRING, ValueType.INTEGER, ValueType.FLOAT, ValueType.BOOLEAN]
98
+
99
+ class ObjectType(TypedDict):
100
+ keyValueSeparator: str
101
+ """the separator used to split the key and value"""
102
+ keyType: Literal[ValueType.STRING, ValueType.INTEGER, ValueType.FLOAT, ValueType.BOOLEAN]
103
+ valueType: Literal[ValueType.STRING, ValueType.INTEGER, ValueType.FLOAT, ValueType.BOOLEAN]
104
+ pairsSeparator: str
105
+ """the separator used to split different key-value pairs"""
106
+
107
+ class BlockType(TypedDict):
108
+ name: str
109
+ """the block type (e.g. "sound", "item", "model"...)"""
110
+ fullType: NotRequired[bool]
111
+ """if true, this should use the module to reference the block"""
112
+ noAutoImport: NotRequired[bool]
113
+ """if true, the will not automatically check its own parent block module when fullType is set to true"""
114
+
115
+ class CallbackType(TypedDict):
116
+ parameters: list['CallbackParameter']
117
+ returns: NotRequired[str]
118
+
119
+ class CallbackParameter(TypedDict):
120
+ name: str
121
+ type: str
122
+ isJava: bool
123
+
124
+
125
+ class ScriptBlockNeeds(TypedDict):
126
+ name: str
127
+ """the dependent parameter"""
128
+ values: NotRequired[list[ScriptBlockValue]]
129
+ """list of possible values for the dependent parameter"""
130
+ valueToType: NotRequired[dict[str, str]]
131
+ """mapping from dependent parameter values to their types
132
+ (for dynamic typing based on other parameter's value)"""
133
+
134
+ ## extra metadata
135
+
136
+ class DeprecatedInfo(TypedDict):
137
+ replacedBy: NotRequired[str]
138
+ description: NotRequired[str]
139
+ version: NotRequired[str]
140
+
141
+ class TranslationProperties(TypedDict):
142
+ """
143
+ Used to indicate translation properties to an element.
144
+ For example a key=value pair where the value needs a specific translation key.
145
+ """
146
+ keyPattern: str
147
+ sourceFile: str
148
+
149
+
150
+ ## block and values base classes
151
+
152
+
153
+ NodeT = TypeVar("NodeT")
154
+
155
+
156
+ class Element(Generic[NodeT]):
157
+ def __init__(self) -> None:
158
+ super().__init__()
159
+ self.node: NodeT
160
+ """AST node of the element."""