diff --git a/docs/requirements.txt b/docs/requirements.txt index 65f582d..f10cdfd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,4 @@ mkdocs-material -mkdocstrings \ No newline at end of file +mkdocstrings +pydantic +websockets \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7a38911..0905d61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -websockets \ No newline at end of file +websockets +pydantic \ No newline at end of file diff --git a/vscode/compiler.py b/vscode/compiler.py index 8561c36..df17c96 100644 --- a/vscode/compiler.py +++ b/vscode/compiler.py @@ -3,13 +3,14 @@ import json import venv import inspect -from typing import TYPE_CHECKING +from pathlib import Path -if TYPE_CHECKING: - from vscode.extension import Extension +from vscode.extension import Launch __all__ = ("build",) +COMMAND = {"title", "category", "command"} + def create_package_json(extension) -> None: package = { @@ -17,14 +18,15 @@ def create_package_json(extension) -> None: "displayName": extension.display_name, "main": "./extension.js", "contributes": { - "commands": [cmd.to_dict() for cmd in extension.commands], + "commands": [ + cmd.dict(include=COMMAND, exclude_unset=True) + for cmd in extension.commands + ], }, - "activationEvents": [ - "onCommand:" + cmd.extension_string for cmd in extension.commands - ], + "activationEvents": ["onCommand:" + cmd.command for cmd in extension.commands], "dependencies": { "ws": "^8.4.0", - } + }, } first_info = { "version": "0.0.1", @@ -56,44 +58,30 @@ def create_package_json(extension) -> None: def create_launch_json(): - launch_json = { - "version": "0.2.0", - "configurations": [ - { - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "args": ["--extensionDevelopmentPath=${workspaceFolder}"], - }, - ], - } - - cwd = os.getcwd() - - vscode_path = os.path.join(cwd, ".vscode") - os.makedirs(vscode_path, exist_ok=True) - os.chdir(vscode_path) - with open("launch.json", "w") as f: - json.dump(launch_json, f, indent=2) + vscode_path = Path.cwd().joinpath(".vscode") + vscode_path.mkdir(exist_ok=True) - os.chdir(cwd) + with open(vscode_path.joinpath("launch.json"), "w") as f: + f.write(Launch().json(indent=2)) REGISTER_COMMANDS_TEMPLATE = """ context.subscriptions.push( - vscode.commands.registerCommand("{}", () => - commandCallback("{}") + vscode.commands.registerCommand("{command}", () => + commandCallback("{name}") ) ); """ + def get_vsc_filepath(file): - return os.path.join(os.path.split(__file__)[0], file) + return Path(__file__).with_name(file) + def create_extension_js(extension): js_code_path = get_vsc_filepath("extcode.js") - if os.path.isfile(js_code_path): + if js_code_path.exists(): with open(js_code_path, "r") as f: code = f.read() else: @@ -106,8 +94,7 @@ def create_extension_js(extension): imports = imports.replace("", file) commands_code = "function registerCommands(context) {\n\t" for cmd in extension.commands: - args = cmd.extension_string, cmd.name - commands_code += REGISTER_COMMANDS_TEMPLATE.format(*args) + commands_code += REGISTER_COMMANDS_TEMPLATE.format(**cmd.dict()) commands_code += "\n}" @@ -120,7 +107,12 @@ def build(extension) -> None: start = time.time() if not os.path.isfile("requirements.txt"): - print(f"\033[1;37;49mA requirements.txt wasn't found in this directory. If your extension has any dependencies kindly put them in the requirements.txt", "\033[0m") + print( + f"\033[1;37;49mA requirements.txt wasn't found in this directory. If your extension has any dependencies kindly put them in the requirements.txt", + "\033[0m", + ) + + # TODO: Add websockets requirement with open("requirements.txt", "w") as f: f.write("git+https://github.com/CodeWithSwastik/vscode-ext@main") @@ -134,7 +126,6 @@ def build(extension) -> None: python_path = os.path.join(os.getcwd(), "venv/bin/python") os.system(f"{python_path} -m pip install -r requirements.txt") - create_launch_json() print(f"\033[1;37;49mCreating package.json...", "\033[0m") create_package_json(extension) @@ -147,4 +138,7 @@ def build(extension) -> None: end = time.time() time_taken = round((end - start), 2) - print(f"\033[1;37;49mBuild completed successfully in {time_taken} seconds! ✨", "\033[0m") + print( + f"\033[1;37;49mBuild completed successfully in {time_taken} seconds! ✨", + "\033[0m", + ) diff --git a/vscode/extcode.py b/vscode/extcode.py index fd6acb5..fe96f4f 100644 --- a/vscode/extcode.py +++ b/vscode/extcode.py @@ -32,7 +32,10 @@ ); } - pyVar = path.join(venvPath, process.platform == "win32" ? "Scripts/python.exe": "bin/python"); + pyVar = path.join( + venvPath, + process.platform == "win32" ? "Scripts/python.exe" : "bin/python" + ); let py = spawn(pyVar, [pythonExtensionPath, "test"]); py.stdout.on("data", (data) => { diff --git a/vscode/extension.py b/vscode/extension.py index be61972..51d954b 100644 --- a/vscode/extension.py +++ b/vscode/extension.py @@ -1,13 +1,17 @@ import sys import asyncio -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, List, Coroutine +import vscode from vscode.context import Context from vscode.wsclient import WSClient -from vscode.compiler import build from vscode.utils import * -__all__ = ("Extension", "Command") +from pydantic import BaseModel, Field, validator, constr + +__all__ = ("Extension", "Command", "Launch") + +SEMVER_REGEX = "^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" class Extension: @@ -42,24 +46,32 @@ def register_command( Register a command. This is usually not called, instead the command() shortcut decorators should be used instead. Args: - func: + func: The function to register as a command. - name: + name: The internal name of the command. - title: + title: The title of the command. This is shown in the command palette. - category: + category: The category that this command belongs to. Default categories set by Extensions will be overriden if this is not None. False should be passed in order to override a default category. - keybind: + keybind: The keybind for this command. - when: + when: A condition for when keybinds should be functional. """ name = func.__name__ if name is None else name category = self.default_category if category is None else category - command = Command(name, func, self, title, category, keybind, when) + command = Command( + name=name, + func=func, + ext=self, + title=title, + category=category, + keybind=keybind, + when=when, + ) if keybind: self.register_keybind(command) self.commands.append(command) @@ -75,17 +87,17 @@ def command( """ A decorator for registering commands. Args: - name: + name: The internal name of the command. - title: + title: The title of the command. This is shown in the command palette. - category: + category: The category that this command belongs to. Default categories set by Extensions will be overriden if this is not None. False should be passed in order to override a default category. - keybind: + keybind: The keybind for this command. - when: + when: A condition for when keybinds should be functional. """ @@ -116,19 +128,19 @@ def run(self): if len(sys.argv) > 1: self.ws.run_webserver() else: - build(self) + vscode.compiler.build(self) async def parse_ws_data(self, data: dict): - if data["type"] == 1: # Command + if data["type"] == 1: # Command name = data.get("name") - if any(name == (cmd:=i).name for i in self.commands): + if any(name == (cmd := i).name for i in self.commands): ctx = Context(ws=self.ws) ctx.command = cmd asyncio.create_task(cmd.func(ctx)) else: print(f"Invalid Command '{name}'", flush=True) - elif data["type"] == 2: # Event + elif data["type"] == 2: # Event event = data.get("event").lower() if event in self.events: event_data = data.get("data") @@ -140,69 +152,83 @@ async def parse_ws_data(self, data: dict): asyncio.create_task(coro) - elif data["type"] == 3: # Eval Response: + elif data["type"] == 3: # Eval Response: self.ws.responses[data["uuid"]] = data.get("res", None) - else: # Unrecognized + else: # Unrecognized print(data, flush=True) -class Command: + +class Command(BaseModel): """ A class that implements the protocol for commands that can be used via the command palette. These should not be created manually, instead they should be created via the decorator or functional interface. """ - def __init__( - self, - name: str, - func: Callable, - ext: Extension, - title: Optional[str] = None, - category: Optional[str] = None, - keybind: Optional[str] = None, - when: Optional[str] = None, - ): - """ - Initialize a command. - Args: - name: - The internal name of the command. - func: - The function to register as a command. - ext: - The extension this command is registered in. - title: - The title of the command. This is shown in the command palette. - category: - The category that this command belongs to. - keybind: - The keybind for this command. - when: - A condition for when keybinds should be functional. - """ + name: str = Field(..., description="The internal name of the command.") + func: Coroutine = Field(..., description="The function to register as a command.") + ext: Extension = Field( + ..., description="The extension this command is registered in." + ) + title: Optional[str] = Field( + description="The title of the command. This is shown in the command palette." + ) + category: Optional[str] = Field( + description="The category that this command belongs to." + ) + keybind: Optional[str] = Field(description="The keybind for this command") + when: Optional[str] = Field( + description="A condition for when keybinds should be functional." + ) + command: Optional[str] = Field( + description="The command to execute when triggered. This field is autogenerated." + ) + func_name: Optional[str] = Field( + description="The function to execute when triggered. This field is autogenerated." + ) + + @validator("name") + def name_convert(cls, v): + return snake_case_to_camel_case(v) + + @validator("title") + def title_convert(cls, v): + return snake_case_to_title_case(v) + + @validator("when") + def when_convert(cls, v): + return python_condition_to_js_condition(v) + + @validator("keybind") + def keybind_convert(cls, v): + return None if v is None else v.upper() + + @validator("func", pre=True) + def func_is_coroutine(cls, v): + print(v.__name__) + print(type(v)) + assert asyncio.iscoroutine(v) + return v + + class Config: + arbitrary_types_allowed = True + + def __init__(self, **data): + super().__init__(**data) - self.name = snake_case_to_camel_case(name) - self.title = snake_case_to_title_case(name) - self.ext = ext + self.func_name = self.func.__name__ + self.command = f"{self.ext.name}.{self.name}" - if not asyncio.iscoroutinefunction(func): - raise TypeError("Callback must be a coroutine.") - self.func = func - self.func_name = self.func.__name__ - self.category = None if category is False else category - self.keybind = keybind.upper() if keybind is not None else None - self.when = python_condition_to_js_condition(when) +class Configuration(BaseModel): + + name: str = "Run Extension" + type: str = "extensionHost" + request: str = "launch" + args: List[str] = ["--extensionDevelopmentPath=${workspaceFolder}"] - def __repr__(self): - return f"" - @property - def extension_string(self) -> str: - return f"{self.ext.name}.{self.name}" +class Launch(BaseModel): - def to_dict(self) -> str: - cmd = {"command": self.extension_string, "title": self.title} - if self.category is not None: - cmd.update({"category": self.category}) - return cmd + version: constr(regex=SEMVER_REGEX) = "0.2.0" + configurations: List[Configuration] = [Configuration()]