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,75 @@
1
+ from typing import TYPE_CHECKING, Self
2
+ if TYPE_CHECKING:
3
+ from ..environment.config import ReleaseModel
4
+
5
+
6
+ class GameVersion:
7
+ _cache: dict[str, 'GameVersion'] = {}
8
+ def __init__(self, version: str):
9
+ self.version = version
10
+ split_version = version.split(".")
11
+ assert len(split_version) >= 3, "Version must have major, minor, and patch components"
12
+ self.major = int(split_version[0])
13
+ self.minor = int(split_version[1])
14
+ self.patch = int(split_version[2])
15
+
16
+ # cache
17
+ GameVersion._cache[version] = self
18
+
19
+ def __hash__(self):
20
+ return hash((self.major, self.minor, self.patch))
21
+
22
+ def __str__(self) -> str:
23
+ return self.version
24
+
25
+ def to_data_version(self, build: int) -> "DataVersion":
26
+ return DataVersion(f"{self.major}.{self.minor}.{self.patch}.{build}")
27
+
28
+ @staticmethod
29
+ def find_or_make(version: str) -> "GameVersion":
30
+ return GameVersion._cache.get(version, GameVersion(version))
31
+
32
+ @staticmethod
33
+ def from_values(major: int, minor: int, patch: int) -> "GameVersion":
34
+ return GameVersion.find_or_make(f"{major}.{minor}.{patch}")
35
+
36
+ def __ge__(self, other):
37
+ if not isinstance(other, GameVersion): return NotImplemented
38
+ return (self.major, self.minor, self.patch) >= (other.major, other.minor, other.patch)
39
+
40
+ def __gt__(self, other):
41
+ if not isinstance(other, GameVersion): return NotImplemented
42
+ return (self.major, self.minor, self.patch) > (other.major, other.minor, other.patch)
43
+
44
+ def __lt__(self, other):
45
+ if not isinstance(other, GameVersion): return NotImplemented
46
+ return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
47
+
48
+ def __le__(self, other):
49
+ if not isinstance(other, GameVersion): return NotImplemented
50
+ return (self.major, self.minor, self.patch) <= (other.major, other.minor, other.patch)
51
+
52
+ def __eq__(self, other):
53
+ if not isinstance(other, GameVersion): return NotImplemented
54
+ return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
55
+
56
+ class DataVersion(GameVersion):
57
+ _data_cache: dict[str, 'DataVersion'] = {}
58
+ def __init__(self, version: str):
59
+ super().__init__(version)
60
+ split_version = version.split(".")
61
+ assert len(split_version) >= 4, "Data version must have major, minor, patch, and build components"
62
+ self.build = int(split_version[3])
63
+
64
+ # cache
65
+ DataVersion._data_cache[version] = self
66
+
67
+ def __hash__(self):
68
+ return hash((self.major, self.minor, self.patch, self.build))
69
+
70
+ def to_game_version(self) -> GameVersion:
71
+ return GameVersion.find_or_make(f"{self.major}.{self.minor}.{self.patch}")
72
+
73
+ @staticmethod
74
+ def find_or_make(version: str) -> "DataVersion":
75
+ return DataVersion._data_cache.get(version, DataVersion(version))
ZedScripts/server.py ADDED
@@ -0,0 +1,288 @@
1
+ import os
2
+ import logging
3
+ from pathlib import Path
4
+
5
+ from lsprotocol import types
6
+ from pygls.lsp.server import LanguageServer
7
+ from pygls.protocol.language_server import LanguageServerProtocol
8
+
9
+ from .__about__ import __version__
10
+ from .utils import uri_to_path
11
+ from .environment import WorkspaceType
12
+ from .environment.workspace import Workspace
13
+ from .environment.document import Document
14
+ from .providers.diagnostics import DiagnosticReport
15
+ from .providers.semantic_tokens import get_tokens
16
+ from .providers.locale import zedlocalizer
17
+ from .providers import capabilities
18
+ from .providers.notifications import ZedNotification, NotificationParams
19
+
20
+
21
+
22
+ # for future implementation of custom protocol features
23
+ class ZedLanguageProtocole(LanguageServerProtocol): ...
24
+
25
+
26
+
27
+ class ZedServer(LanguageServer):
28
+ instance: 'ZedServer | None' = None
29
+ def __init__(self):
30
+ super().__init__(name="zedserver", version=__version__, protocol_cls=ZedLanguageProtocole)
31
+ self.documents: dict[Path, Document] = {}
32
+ zedlocalizer.load_locale_files()
33
+ ZedServer.instance = self
34
+
35
+ def send_notification(self, method: ZedNotification, params: NotificationParams | None = None) -> None:
36
+ logging.debug("Sending notification: %s with params: %s", method, params)
37
+ self.protocol.notify(
38
+ method,
39
+ params
40
+ )
41
+
42
+ # def send_request(self, method: str, )
43
+
44
+
45
+ def wait_for_debug_client(self) -> None:
46
+ import time
47
+ from .providers.debug import wait_for_port_available, force_close_port
48
+ try:
49
+ import debugpy
50
+ except ImportError:
51
+ logging.error("debugpy is not installed. Debugging will not be available.")
52
+ return
53
+
54
+ debug_port = 5678
55
+ debug_host = "localhost"
56
+
57
+ # ensure the debug port is available
58
+ if not wait_for_port_available(debug_port, debug_host):
59
+ logging.error(f"Debug port {debug_port} is still in use after waiting. Attempting to bind anyway...")
60
+
61
+ # probably a bad idea to do that
62
+ # but realistically, we should only be the ones using that port
63
+ force_close_port(debug_port)
64
+ time.sleep(0.2)
65
+
66
+
67
+ debugpy.listen((debug_host, debug_port))
68
+ logging.debug(f"Waiting for debug client to attach on {debug_host}:{debug_port}...")
69
+ debugpy.wait_for_client() # blocks until the "Python: Attach" launch config connects
70
+ logging.debug("Debug client attached.")
71
+
72
+
73
+ def startup(self) -> None:
74
+ logging.info(f"ZedServer {__version__} starting up.")
75
+
76
+ logging.debug("ZEDSCRIPTS_DEBUG_WAIT=%s", os.environ.get("ZEDSCRIPTS_DEBUG_WAIT"))
77
+ if os.environ.get("ZEDSCRIPTS_DEBUG_WAIT") == "1":
78
+ self.wait_for_debug_client()
79
+
80
+ self.start_io()
81
+
82
+
83
+
84
+ def on_document_changed(self, path: Path, text: str) -> None:
85
+ # skip if it's a configuration file
86
+ if Workspace.update_configuration_file(path, text):
87
+ return
88
+
89
+ # first check if the file is a zedscripts file
90
+ document = Workspace.find_or_make(path)
91
+ if document is None:
92
+ return
93
+
94
+ logging.debug("Document changed: %s\n%s", path, text)
95
+
96
+ # update it
97
+ document.on_document_changed(text)
98
+
99
+ def on_document_deleted(self, path: Path) -> None:
100
+ Document.delete(path)
101
+
102
+ def on_document_renamed(self, old_file: Path, new_file: Path) -> None:
103
+ Document.rename(old_file, new_file)
104
+
105
+ def on_did_change_watched_files(self, params: types.DidChangeWatchedFilesParams) -> None:
106
+ # we are only interested in handling the global configuration file changes
107
+ for change in params.changes:
108
+ path = uri_to_path(change.uri)
109
+ if not Workspace.is_global_configuration_file(path):
110
+ continue
111
+
112
+ logging.debug(f"Global configuration file changed. ({change.type})")
113
+
114
+ # update the global configuration
115
+ Workspace.update_global_configuration()
116
+
117
+
118
+ def on_document_diagnostics(self, path: Path, previous_result_id: str | None) -> DiagnosticReport | None:
119
+ document = Workspace.find_or_make(path)
120
+ if document is None:
121
+ return None
122
+ return document.on_document_diagnostics(previous_result_id)
123
+
124
+ def on_semantic_tokens(self, path: Path) -> types.SemanticTokens | None:
125
+ document = Document.find(path)
126
+ if document is None:
127
+ return None
128
+ return document.on_semantic_tokens()
129
+
130
+ def on_document_hover(self, path: Path, position: types.Position) -> types.Hover | None:
131
+ document = Document.find(path)
132
+ if document is None:
133
+ return None
134
+ return document.on_hover(position)
135
+
136
+ def on_linked_editing_range(self, path: Path, position: types.Position) -> types.LinkedEditingRanges | None:
137
+ document = Document.find(path)
138
+ if document is None:
139
+ return None
140
+ return document.on_linked_editing_range(position)
141
+
142
+ def on_definition(self, path: Path, position: types.Position) -> types.DefinitionResult:
143
+ document = Document.find(path)
144
+ if document is None:
145
+ return None
146
+ return document.on_definition(position)
147
+
148
+ zedserver = ZedServer()
149
+
150
+
151
+
152
+ ## EVENT HANDLERS
153
+
154
+ @zedserver.feature(types.INITIALIZE)
155
+ def initialize(server: ZedServer, params: types.InitializeParams):
156
+ """
157
+ In here we handle the initialization of the server. For that we provide
158
+ a set of rules and capabilities that the server supports.
159
+
160
+ ### Diagnostics
161
+ Our diagnostics depend on other files so we prefer to diagnostic
162
+ the whole workspace. They could take a while for large workspaces
163
+ (i.e. the game files) so we want to provide feedback when it takes
164
+ a while.
165
+ """
166
+ # this will store what the server can currently do
167
+ capabilities.register_client_capabilities(params)
168
+
169
+ # from the above, we determine what the server capabilities should be
170
+ return types.InitializeResult(
171
+ capabilities=capabilities.get_server_capabilities(),
172
+ server_info=types.ServerInfo(
173
+ name="ZedScripts Language Server",
174
+ version=__version__,
175
+ )
176
+ )
177
+
178
+
179
+ @zedserver.feature(types.INITIALIZED)
180
+ def initialized(server: ZedServer, params: types.InitializedParams) -> None:
181
+ """
182
+ Called after the client sends the 'initialized' notification.
183
+ At this point, the client is ready to receive notifications.
184
+ This is the proper place to load workspaces and send progress notifications.
185
+ """
186
+ # load the global dataset
187
+ Workspace.global_dataset.load()
188
+
189
+ logging.debug("Server workspace folders")
190
+ logging.debug(server.workspace.folders)
191
+ for workspace_folder in server.workspace.folders.values():
192
+ ws = Workspace(uri_to_path(workspace_folder.uri), WorkspaceType.PROJECT)
193
+ ws.load()
194
+ Workspace.load_libraries()
195
+ server.send_notification(
196
+ ZedNotification.LOADING_DOCUMENTS_DONE
197
+ )
198
+
199
+
200
+ @zedserver.feature(types.TEXT_DOCUMENT_DID_OPEN)
201
+ def did_open(server: ZedServer, params: types.DidOpenTextDocumentParams) -> None:
202
+ server.on_document_changed(uri_to_path(params.text_document.uri), params.text_document.text)
203
+
204
+ @zedserver.feature(types.TEXT_DOCUMENT_DID_CHANGE)
205
+ def did_change(server: ZedServer, params: types.DidChangeTextDocumentParams) -> None:
206
+ document = server.workspace.get_text_document(params.text_document.uri)
207
+ server.on_document_changed(uri_to_path(document.uri), str.join("", document.lines))
208
+
209
+ @zedserver.feature(types.WORKSPACE_DID_DELETE_FILES)
210
+ def workspace_did_delete_files(server: ZedServer, params: types.DeleteFilesParams) -> None:
211
+ for file in params.files:
212
+ path = uri_to_path(file.uri)
213
+ server.on_document_deleted(path)
214
+
215
+ @zedserver.feature(types.WORKSPACE_DID_RENAME_FILES)
216
+ def workspace_did_rename_files(server: ZedServer, params: types.RenameFilesParams) -> None:
217
+ for file in params.files:
218
+ old_path = uri_to_path(file.old_uri)
219
+ new_path = uri_to_path(file.new_uri)
220
+ server.on_document_renamed(old_path, new_path)
221
+
222
+
223
+
224
+ @zedserver.feature(types.TEXT_DOCUMENT_DIAGNOSTIC)
225
+ def diagnostic(server: ZedServer, params: types.DocumentDiagnosticParams) -> DiagnosticReport | None:
226
+ path = uri_to_path(params.text_document.uri)
227
+ return server.on_document_diagnostics(path, params.previous_result_id)
228
+
229
+
230
+
231
+ # sadly I'm not sure that implementation works as expected because the client constantly
232
+ # asks again and again for workspace diagnostics
233
+ # it also doesn't ask for diagnostics of non-opened documents
234
+ #
235
+ # # also see when implementing the configuration file:
236
+ # # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#diagnostic_refresh
237
+ # @zedserver.feature(types.WORKSPACE_DIAGNOSTIC)
238
+ # def workspace_diagnostic(server: ZedServer, params: types.WorkspaceDiagnosticParams):
239
+ # """
240
+ # Provides diagnostics for all documents in the workspace.
241
+ # This is called when the client requests workspace-wide diagnostics.
242
+ # """
243
+ # logging.info("Workspace diagnostic requested.")
244
+
245
+ # # map of previous result IDs by document URI
246
+ # previous_result_ids = {item.uri: item.value for item in params.previous_result_ids}
247
+ # logging.debug(pformat(previous_result_ids))
248
+
249
+ # items: list[types.WorkspaceDocumentDiagnosticReport] = []
250
+ # for document in Document.get_documents():
251
+ # uri = document.get_uri()
252
+ # previous_result_id = previous_result_ids.get(uri)
253
+ # items.append(document.get_lsp_workspace_diagnostics(previous_result_id))
254
+
255
+ # return types.WorkspaceDiagnosticReport(items=items)
256
+
257
+
258
+ token_types, token_modifiers = get_tokens()
259
+ @zedserver.feature(
260
+ types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL,
261
+ types.SemanticTokensLegend(
262
+ token_types=token_types,
263
+ token_modifiers=token_modifiers,
264
+ )
265
+ )
266
+ def semantic_tokens(server: ZedServer, params: types.SemanticTokensParams):
267
+ path = uri_to_path(params.text_document.uri)
268
+ return server.on_semantic_tokens(path)
269
+
270
+
271
+ @zedserver.feature(types.TEXT_DOCUMENT_HOVER)
272
+ def hover(server: ZedServer, params: types.HoverParams) -> types.Hover | None:
273
+ path = uri_to_path(params.text_document.uri)
274
+ return server.on_document_hover(path, params.position)
275
+
276
+ @zedserver.feature(types.TEXT_DOCUMENT_LINKED_EDITING_RANGE)
277
+ def linked_editing_range(server: ZedServer, params: types.LinkedEditingRangeParams) -> types.LinkedEditingRanges | None:
278
+ path = uri_to_path(params.text_document.uri)
279
+ return server.on_linked_editing_range(path, params.position)
280
+
281
+ @zedserver.feature(types.TEXT_DOCUMENT_DEFINITION)
282
+ def definition(server: ZedServer, params: types.DefinitionParams) -> types.DefinitionResult:
283
+ path = uri_to_path(params.text_document.uri)
284
+ return server.on_definition(path, params.position)
285
+
286
+ @zedserver.feature(types.WORKSPACE_DID_CHANGE_WATCHED_FILES)
287
+ def did_change_watched_files(server: ZedServer, params: types.DidChangeWatchedFilesParams) -> None:
288
+ return server.on_did_change_watched_files(params)
@@ -0,0 +1,143 @@
1
+ from typing import TYPE_CHECKING, Optional
2
+
3
+ from .lexer import Token, TokenCollection
4
+
5
+ if TYPE_CHECKING:
6
+ from .lexer import TextPosition
7
+
8
+ class BlockBody:
9
+ def __init__(self) -> None:
10
+ super().__init__()
11
+ self.children: list[Node] = []
12
+ """Child nodes."""
13
+
14
+
15
+ class Node:
16
+ """Base class for Zedscript AST nodes."""
17
+ def __init__(self, *, comments: Optional[list[Token]] = None) -> None:
18
+ super().__init__()
19
+ if comments is None:
20
+ comments = []
21
+ self.comments: list[Token] = comments
22
+ """
23
+ Comments for the node.
24
+ For blocks, all comments before the opening bracket are included.
25
+ For values, all comments in the range of the value are included.
26
+ A TokenCollection is not used because the tokens might not be contiguous.
27
+ """
28
+
29
+
30
+ class BlockNode(Node, BlockBody):
31
+ """
32
+ Zedscript block.
33
+ """
34
+ def __init__(
35
+ self, type: TokenCollection | None, id: TokenCollection | None,
36
+ open_bracket: Token, close_bracket: Token | None,
37
+ *, comments: Optional[list[Token]] = None
38
+ ) -> None:
39
+ super().__init__(comments=comments)
40
+ self.type: TokenCollection | None = type
41
+ """
42
+ Tokens representing the type of the block.
43
+ This never has any whitespace, but a collection is used because comments can appear anywhere.
44
+ This will never be None in well formed Zedscript.
45
+ """
46
+ self.id: TokenCollection | None = id
47
+ """
48
+ Tokens representing the id of the block.
49
+ This never has any whitespace, but a collection is used because comments can appear anywhere.
50
+ """
51
+ self.open_bracket: Token = open_bracket
52
+ """
53
+ Opening bracket token. (``{``)
54
+ """
55
+ self.close_bracket: Token | None = close_bracket
56
+ """
57
+ Closing bracket token. (``}``)
58
+ This will never be None in well formed Zedscript.
59
+ """
60
+
61
+ @property
62
+ def tokens(self) -> TokenCollection:
63
+ result = TokenCollection()
64
+ if self.type is not None:
65
+ result.extend(self.type)
66
+ if self.id is not None:
67
+ result.extend(self.id)
68
+ result.append(self.open_bracket)
69
+ if self.close_bracket is not None:
70
+ result.append(self.close_bracket)
71
+ return result
72
+
73
+ def well_formed(self) -> bool:
74
+ return self.type is not None and self.close_bracket is not None
75
+
76
+ def contains_position(self, text_position: 'TextPosition') -> bool:
77
+ return self.tokens.to_range() == text_position
78
+
79
+
80
+ class ValueNode(Node):
81
+ """
82
+ Zedscript value.
83
+ """
84
+ def __init__(self, tokens: TokenCollection, comma: Token, *, comments: Optional[list[Token]] = None) -> None:
85
+ super().__init__(comments=comments)
86
+ self.tokens: TokenCollection = tokens
87
+ """Tokens comprising the value."""
88
+ self.comma: Token = comma
89
+ """
90
+ Comma token following the value.
91
+ """
92
+
93
+ def is_key_value(self) -> bool:
94
+ return "=" in str(self.tokens)
95
+
96
+ def value(self) -> TokenCollection:
97
+ if not self.is_key_value():
98
+ return self.tokens.strip()
99
+ value = TokenCollection()
100
+
101
+ # for token in reversed(self.tokens):
102
+ found_equal = False
103
+ for token in self.tokens:
104
+ if not found_equal:
105
+ if token.text == "=":
106
+ found_equal = True
107
+ continue
108
+ # if token.text == "=":
109
+ # found_equal = True
110
+ # break
111
+ value.append(token)
112
+
113
+ # since we looped backwards, they've been inserted backwards
114
+ # value.reverse()
115
+ return value.strip()
116
+
117
+ def key(self) -> TokenCollection:
118
+ assert self.is_key_value()
119
+ key = TokenCollection()
120
+
121
+ for token in self.tokens:
122
+ if token.text == "=":
123
+ break
124
+ key.append(token)
125
+
126
+ return key.strip()
127
+
128
+ def equals(self) -> Token:
129
+ assert self.is_key_value()
130
+ for token in self.tokens:
131
+ if token.text == "=":
132
+ return token
133
+ raise RuntimeError("Couldn't find equals token")
134
+
135
+ def contains_position(self, text_position: 'TextPosition') -> bool:
136
+ return self.tokens.to_range() == text_position
137
+
138
+
139
+ class Chunk(BlockBody):
140
+ """
141
+ Root of a Zedscript chunk.
142
+ A chunk is essentially a block body, but we use a separate class to avoid ambiguity.
143
+ """