From d190d1b1f17d5a1fad6d3c8a5e6f96956c6283c6 Mon Sep 17 00:00:00 2001 From: sea-odoo <54021048+sea-odoo@users.noreply.github.com> Date: Mon, 22 Dec 2025 11:23:20 +0100 Subject: [PATCH 1/4] [IMP] Relocating plugin files to the configuration directory (#2) * [REF] Switch folder to python package (move out plugins of odev path) * [REF] Remove useless ignore --- .gitignore | 1 - __init__.py | 1 + common/__init__.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 __init__.py create mode 100644 common/__init__.py diff --git a/.gitignore b/.gitignore index 79a9498..3a47d02 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,4 @@ __pycache__ htmlcov # --- Odev plugins, loaded as submodules -odev/plugins/* tests/plugins/* diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..effaf3b --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from .common import editor_vscode # noqa: F401 diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000..e69de29 From 9e4296fa9a4ea93531ffd15da94a42bc5cd8f0f4 Mon Sep 17 00:00:00 2001 From: avs-odoo Date: Tue, 23 Dec 2025 10:00:55 +0100 Subject: [PATCH 2/4] [IMP] Suport standard modules for a version and codium derivatives (#3) Add support for opening a workspace containing only standard modules for a specific Odoo version. Also add support for other editors sharing the same base as VSCode (i.e.: VSCodium, Antigravity). --- README.md | 12 +++++- __manifest__.py | 2 +- common/editor_vscode.py | 88 ++++++++++++++++++++++++++--------------- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 6e33156..a882543 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # ODEV - VSCode Editor -Configure VSCode for a database and open a repository in the editor. +Configure VSCode or any other editor derivated from VSCodium (e.g. VSCodium) for a database and open a repository +in the editor. ## Installation @@ -12,3 +13,12 @@ Enable this plugin by running: ```bash odev plugin --enable odoo-odev/odev-plugin-editor-vscode ``` + +Using other editors with the same base (e.g. VSCodium) should work as well at the cost of setting up a symlink +to the editor executable in your path. + +Antigravity example: + +```bash +ln -s $(which antigravity) /usr/local/bin/code +``` diff --git a/__manifest__.py b/__manifest__.py index 39d6588..c4fb61a 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -22,7 +22,7 @@ # or merged change. # ------------------------------------------------------------------------------ -__version__ = "1.1.0" +__version__ = "1.2.0" # --- Dependencies ------------------------------------------------------------- # List other odev plugins from which this current plugin depends. diff --git a/common/editor_vscode.py b/common/editor_vscode.py index e1cddf2..69accfb 100644 --- a/common/editor_vscode.py +++ b/common/editor_vscode.py @@ -1,16 +1,14 @@ import json from pathlib import Path -from typing import cast -from odev.common import progress, string +from odev.common import bash, progress, string from odev.common.console import console from odev.common.databases import LocalDatabase from odev.common.logging import logging +from odev.common.odoobin import OdoobinProcess from odev.common.python import PythonEnv - from odev.plugins.odev_plugin_editor_base.common.editor import Editor - logger = logging.getLogger(__name__) @@ -20,22 +18,34 @@ class VSCodeEditor(Editor): _name = "code" _display_name = "VSCode" + @property + def display_name(self) -> str: + """Also handle other editors derivated from VSCodium (e.g. Antigravity).""" + name = bash.execute(f"{self._name} --help | head -n 1 | awk '{{print $1}}'") + + if name and name.stdout.strip(): + return name.stdout.strip().capitalize().decode() + + return self._display_name + @property def command(self) -> str: - if isinstance(self.database, LocalDatabase): - return f"{self._name} {self.workspace_path}" - else: - return super().command + return f"{self._name} {self.workspace_path}" @property def workspace_directory(self) -> Path: """The path to the workspace directory.""" - return self.path / ".vscode" + return ( + self.path / ".vscode" + if isinstance(self.database, LocalDatabase) + else self.path + ) @property def workspace_path(self) -> Path: """The path to the workspace file.""" - return self.workspace_directory / f"{self.database.name}.code-workspace" + name = self.database.name if isinstance(self.database, LocalDatabase) else str(self.version) + return self.workspace_directory / f"{name}.code-workspace" @property def launch_path(self) -> Path: @@ -49,12 +59,16 @@ def tasks_path(self) -> Path: def configure(self): """Configure VSCode to work with the database.""" - if not isinstance(self.database, LocalDatabase): + if not isinstance(self.database, LocalDatabase) and not self.version: return logger.warning( - f"No local database associated with repository {self.git.name!r}, skipping VSCode configuration" + f"No local database associated with repository {self.git.name!r}, " + f"skipping {self.display_name} configuration" ) - with progress.spinner(f"Configuring {self._display_name} for project {self.git.name!r}"): + if self.workspace_path.is_file(): + return logger.debug("Workspace file already exists") + + with progress.spinner(f"Configuring {self.display_name} for project {self.git.name!r}"): self.workspace_directory.mkdir(parents=True, exist_ok=True) missing_files = filter( @@ -63,40 +77,52 @@ def configure(self): ) if not list(missing_files): - return logger.debug("VSCode config files already exist") + return logger.debug(f"{self.display_name} config files already exist") - self._create_workspace() - self._create_launch() - self._create_tasks() + created_files_list = [] - created_files = string.join_bullet( - [ - f"Workspace: {self.workspace_path}", + if self._create_workspace(): + created_files_list.append(f"Workspace: {self.workspace_path}") + + if not self.version: + self._create_launch() + self._create_tasks() + created_files_list.extend([ f"Debugging: {self.launch_path}", f"Tasks: {self.tasks_path}", - ], - ) - logger.info(f"Created VSCode config for project {self.git.name!r}\n{created_files}") + ]) - def _create_workspace(self): - """Create a workspace file for the project.""" - assert isinstance(self.database, LocalDatabase) + created_files = string.join_bullet(created_files_list) + logger.info(f"Created {self.display_name} config for project {self.git.name!r}\n{created_files}") + def _create_workspace(self) -> bool: + """Create a workspace file for the project.""" if self.workspace_path.is_file(): - return logger.debug("Workspace file already exists") + logger.debug("Workspace file already exists") + return False workspace_config = { - "folders": [{"path": ".."}], + "folders": [], "settings": { "terminal.integrated.cwd": self.path.as_posix(), - "python.defaultInterpreterPath": self.database.venv.python.as_posix(), }, } - for worktree in self.database.worktrees: - cast(list, workspace_config["folders"]).append({"path": worktree.path.as_posix()}) + process = ( + self.database.process + if isinstance(self.database, LocalDatabase) and self.database.process + else OdoobinProcess(self.database, self.version).with_edition("enterprise") + ) + + if isinstance(self.database, LocalDatabase): + workspace_config["folders"].append({"path": ".."}) + workspace_config["settings"]["python.defaultInterpreterPath"] = self.database.venv.python.as_posix() + + for worktree in process.odoo_worktrees: + workspace_config["folders"].append({"path": worktree.path.as_posix()}) console.print(json.dumps(workspace_config, indent=4), file=self.workspace_path) + return True def _create_launch(self): """Create a launch file for the project.""" From 91a7a3f1e9c7fe0e47a2c129230e5405e5aaa4e3 Mon Sep 17 00:00:00 2001 From: sea-odoo <54021048+sea-odoo@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:46:15 +0200 Subject: [PATCH 3/4] [FIX] vscode: improve launch/tasks configuration generation (#5) This commit fixes several issues in the VSCode configuration generation: - Corrected the template directory lookup path. - Updated the Python interpreter path to use the actual virtualenv of the Odoo database instead of a generic fallback. - Switched to Path.write_text for writing JSON configuration files to avoid formatting issues. - Enabled overwriting of existing .vscode configurations to ensure they stay in sync with database settings. --- __manifest__.py | 2 +- common/editor_vscode.py | 91 +++++++++++++++++++++++------------------ 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/__manifest__.py b/__manifest__.py index c4fb61a..87adef2 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -22,7 +22,7 @@ # or merged change. # ------------------------------------------------------------------------------ -__version__ = "1.2.0" +__version__ = "1.2.1" # --- Dependencies ------------------------------------------------------------- # List other odev plugins from which this current plugin depends. diff --git a/common/editor_vscode.py b/common/editor_vscode.py index 69accfb..b69823e 100644 --- a/common/editor_vscode.py +++ b/common/editor_vscode.py @@ -2,11 +2,9 @@ from pathlib import Path from odev.common import bash, progress, string -from odev.common.console import console from odev.common.databases import LocalDatabase from odev.common.logging import logging from odev.common.odoobin import OdoobinProcess -from odev.common.python import PythonEnv from odev.plugins.odev_plugin_editor_base.common.editor import Editor logger = logging.getLogger(__name__) @@ -65,69 +63,84 @@ def configure(self): f"skipping {self.display_name} configuration" ) - if self.workspace_path.is_file(): - return logger.debug("Workspace file already exists") + # We always want to update the configuration to ensure the Python environment is correct + # even if the workspace file already exists. with progress.spinner(f"Configuring {self.display_name} for project {self.git.name!r}"): self.workspace_directory.mkdir(parents=True, exist_ok=True) - missing_files = filter( - lambda path: not path.is_file(), - [self.workspace_path, self.launch_path, self.tasks_path], - ) - - if not list(missing_files): - return logger.debug(f"{self.display_name} config files already exist") - created_files_list = [] if self._create_workspace(): created_files_list.append(f"Workspace: {self.workspace_path}") - if not self.version: - self._create_launch() - self._create_tasks() - created_files_list.extend([ - f"Debugging: {self.launch_path}", - f"Tasks: {self.tasks_path}", - ]) + self._create_launch() + self._create_tasks() + created_files_list.extend([ + f"Debugging: {self.launch_path}", + f"Tasks: {self.tasks_path}", + ]) created_files = string.join_bullet(created_files_list) logger.info(f"Created {self.display_name} config for project {self.git.name!r}\n{created_files}") def _create_workspace(self) -> bool: """Create a workspace file for the project.""" + workspace_config = {} if self.workspace_path.is_file(): - logger.debug("Workspace file already exists") - return False - - workspace_config = { - "folders": [], - "settings": { - "terminal.integrated.cwd": self.path.as_posix(), - }, - } + try: + workspace_config = json.loads(self.workspace_path.read_text()) + except Exception: + logger.warning(f"Could not load existing workspace file {self.workspace_path}") + + if not workspace_config: + workspace_config = { + "folders": [], + "settings": {}, + } + + workspace_config["settings"]["terminal.integrated.cwd"] = self.path.as_posix() process = ( self.database.process if isinstance(self.database, LocalDatabase) and self.database.process - else OdoobinProcess(self.database, self.version).with_edition("enterprise") + else OdoobinProcess(self.database, version=self.version).with_edition("enterprise") ) if isinstance(self.database, LocalDatabase): - workspace_config["folders"].append({"path": ".."}) - workspace_config["settings"]["python.defaultInterpreterPath"] = self.database.venv.python.as_posix() + if {"path": ".."} not in workspace_config["folders"]: + workspace_config["folders"].append({"path": ".."}) + + python_path = process.venv.python.as_posix() + workspace_config["settings"]["python.defaultInterpreterPath"] = python_path + # Set interpreterPath as well for better compatibility with different editor versions + workspace_config["settings"]["python.interpreterPath"] = python_path + + # Add extra paths for better autocompletion + extra_paths = [p.as_posix() for p in process.addons_paths if p.exists()] + workspace_config["settings"]["python.analysis.extraPaths"] = extra_paths + workspace_config["settings"]["python.autoComplete.extraPaths"] = extra_paths + + # Force Ruff extension to use the binary from the venv + ruff_bin = (process.venv.path / "bin" / "ruff").as_posix() + workspace_config["settings"]["ruff.path"] = [ruff_bin] + workspace_config["settings"]["ruff.importStrategy"] = "fromEnvironment" for worktree in process.odoo_worktrees: - workspace_config["folders"].append({"path": worktree.path.as_posix()}) + worktree_path = {"path": worktree.path.as_posix()} + if worktree_path not in workspace_config["folders"]: + workspace_config["folders"].append(worktree_path) - console.print(json.dumps(workspace_config, indent=4), file=self.workspace_path) + self.workspace_path.write_text(json.dumps(workspace_config, indent=4)) return True def _create_launch(self): """Create a launch file for the project.""" - if self.launch_path.is_file(): - return logger.debug("Launch file already exists") + process = ( + self.database.process + if isinstance(self.database, LocalDatabase) and self.database.process + else OdoobinProcess(self.database, version=self.version).with_edition("enterprise") + ) def run_config(shell: bool = False): title = "Shell" if shell else "Run" @@ -141,7 +154,7 @@ def run_config(shell: bool = False): "consoleName": f"Odev {title} ({self.database.name})", "cwd": self.path.as_posix(), "program": self.database.odev.executable.as_posix(), - "python": PythonEnv().python.as_posix(), + "python": process.venv.python.as_posix(), "args": [ title.lower(), self.database.name, @@ -165,16 +178,14 @@ def run_config(shell: bool = False): ], } - console.print(json.dumps(launch_config, indent=4), file=self.launch_path) + self.launch_path.write_text(json.dumps(launch_config, indent=4)) def _create_tasks(self): """Create a tasks file for the project.""" - if self.tasks_path.is_file(): - return logger.debug("Tasks file already exists") tasks_config = { "version": "2.0.0", "tasks": [], } - console.print(json.dumps(tasks_config, indent=4), file=self.tasks_path) + self.tasks_path.write_text(json.dumps(tasks_config, indent=4)) From cd76dc9413f847f284b68e85a75d6b2d3ef831e0 Mon Sep 17 00:00:00 2001 From: Armin-FalDiS Date: Mon, 8 Jun 2026 15:56:14 +0200 Subject: [PATCH 4/4] [IMP] editor_vscode: config from jinja templates - Rework the VSCode configuration generation to render the workspace, launch, tasks and jsconfig files from jinja templates instead of building the dictionaries inline - Add jsconfig.json for JS/Owl intellisense --- __manifest__.py | 2 +- common/editor_vscode.py | 256 ++++++++++++++++++--------------- config.py | 20 +++ templates/code-workspace.jinja | 50 +++++++ templates/jsconfig.jinja | 41 ++++++ templates/launch.jinja | 125 ++++++++++++++++ templates/tasks.jinja | 207 ++++++++++++++++++++++++++ 7 files changed, 583 insertions(+), 118 deletions(-) create mode 100644 config.py create mode 100644 templates/code-workspace.jinja create mode 100644 templates/jsconfig.jinja create mode 100644 templates/launch.jinja create mode 100644 templates/tasks.jinja diff --git a/__manifest__.py b/__manifest__.py index 87adef2..bec5f51 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -22,7 +22,7 @@ # or merged change. # ------------------------------------------------------------------------------ -__version__ = "1.2.1" +__version__ = "2.0.0" # --- Dependencies ------------------------------------------------------------- # List other odev plugins from which this current plugin depends. diff --git a/common/editor_vscode.py b/common/editor_vscode.py index b69823e..7c8bcb3 100644 --- a/common/editor_vscode.py +++ b/common/editor_vscode.py @@ -1,12 +1,19 @@ import json +import os +import re from pathlib import Path +from jinja2 import Environment, FileSystemLoader + from odev.common import bash, progress, string from odev.common.databases import LocalDatabase from odev.common.logging import logging from odev.common.odoobin import OdoobinProcess +from odev.common.python import PythonEnv + from odev.plugins.odev_plugin_editor_base.common.editor import Editor + logger = logging.getLogger(__name__) @@ -19,31 +26,57 @@ class VSCodeEditor(Editor): @property def display_name(self) -> str: """Also handle other editors derivated from VSCodium (e.g. Antigravity).""" - name = bash.execute(f"{self._name} --help | head -n 1 | awk '{{print $1}}'") + result = bash.execute(f"{self._name} --help", raise_on_error=False) - if name and name.stdout.strip(): - return name.stdout.strip().capitalize().decode() + if result and result.stdout: + first_line = result.stdout.decode().splitlines()[0] + name = re.sub(r"\s+v?[\d.]+.*$", "", first_line).strip() + + if name: + return name return self._display_name + @property + def process(self) -> OdoobinProcess: + """Odoo process backing the configuration. + + Falls back to a version-based process when no local database exists, so that + repositories opened without a database (version-only) can still be configured. + """ + if isinstance(self.database, LocalDatabase) and self.database.process: + return self.database.process + return OdoobinProcess(self.database, version=self.version).with_edition("enterprise") + @property def command(self) -> str: return f"{self._name} {self.workspace_path}" + @property + def templates(self) -> Environment: + return Environment( # noqa: S701 + loader=FileSystemLoader(self.database.odev.plugins_path / "odev_plugin_editor_vscode/templates") + ) + + @property + def odoo_path(self) -> Path: + """The path to the worktree holding the Odoo sources (odoo, enterprise, ...).""" + return self.database.odev.worktrees_path / self.process.worktree + @property def workspace_directory(self) -> Path: """The path to the workspace directory.""" - return ( - self.path / ".vscode" - if isinstance(self.database, LocalDatabase) - else self.path - ) + return self.path / ".vscode" if isinstance(self.database, LocalDatabase) else self.path + + @property + def workspace_name(self) -> str: + """The base name used for the workspace file.""" + return self.database.name if isinstance(self.database, LocalDatabase) else str(self.version) @property def workspace_path(self) -> Path: """The path to the workspace file.""" - name = self.database.name if isinstance(self.database, LocalDatabase) else str(self.version) - return self.workspace_directory / f"{name}.code-workspace" + return self.workspace_directory / f"{self.workspace_name}.code-workspace" @property def launch_path(self) -> Path: @@ -63,129 +96,118 @@ def configure(self): f"skipping {self.display_name} configuration" ) - # We always want to update the configuration to ensure the Python environment is correct - # even if the workspace file already exists. + config_files = [self.workspace_path, self.launch_path, self.tasks_path, self.path / "jsconfig.json"] + + if all(path.is_file() for path in config_files): + logger.debug(f"{self.display_name} config files already exist, skipping configuration") + return None with progress.spinner(f"Configuring {self.display_name} for project {self.git.name!r}"): self.workspace_directory.mkdir(parents=True, exist_ok=True) - created_files_list = [] - - if self._create_workspace(): - created_files_list.append(f"Workspace: {self.workspace_path}") - + self._create_workspace() self._create_launch() self._create_tasks() - created_files_list.extend([ - f"Debugging: {self.launch_path}", - f"Tasks: {self.tasks_path}", - ]) + self._create_jsconfig() - created_files = string.join_bullet(created_files_list) + created_files = string.join_bullet( + [ + f"Workspace: {self.workspace_path}", + f"Launch: {self.launch_path}", + f"Tasks: {self.tasks_path}", + ], + ) logger.info(f"Created {self.display_name} config for project {self.git.name!r}\n{created_files}") + return None + + def _get_rendered_template(self, template_name, **kwargs): + template = self.templates.get_template(template_name) + return template.render(kwargs) - def _create_workspace(self) -> bool: + @property + def workspace_folders(self) -> list[dict]: + """The multi-root workspace folders, depending on the configured layout. + + - 'flat' (default): one top-level root per odoo worktree alongside the project. + - 'nested': a single 'odoo' root holding all worktrees as subfolders. + """ + project_folder = {"path": "..", "name": "project"} + layout = self.database.odev.config.vscode.workspace_layout + + if layout == "flat": + return [ + project_folder, + *( + {"path": worktree.path.as_posix(), "name": worktree.path.name} + for worktree in self.process.odoo_worktrees + ), + ] + + return [project_folder, {"path": self.odoo_path.as_posix(), "name": "odoo"}] + + def _create_workspace(self): """Create a workspace file for the project.""" - workspace_config = {} - if self.workspace_path.is_file(): - try: - workspace_config = json.loads(self.workspace_path.read_text()) - except Exception: - logger.warning(f"Could not load existing workspace file {self.workspace_path}") - - if not workspace_config: - workspace_config = { - "folders": [], - "settings": {}, - } - - workspace_config["settings"]["terminal.integrated.cwd"] = self.path.as_posix() - - process = ( - self.database.process - if isinstance(self.database, LocalDatabase) and self.database.process - else OdoobinProcess(self.database, version=self.version).with_edition("enterprise") + rendered_template = self._get_rendered_template( + "code-workspace.jinja", + DB_NAME=self.workspace_name, + ODOO_PATH=self.odoo_path, + FOLDERS=json.dumps(self.workspace_folders, indent=4), + VENV_PATH=self.process.venv.python.as_posix(), + RUFF_PATH=(self.process.venv.path / "bin" / "ruff").as_posix(), + PYTHON_PATH=PythonEnv().python.as_posix(), + ODEV_EXE_PATH=self.database.odev.executable.with_name("main.py").as_posix(), ) - - if isinstance(self.database, LocalDatabase): - if {"path": ".."} not in workspace_config["folders"]: - workspace_config["folders"].append({"path": ".."}) - - python_path = process.venv.python.as_posix() - workspace_config["settings"]["python.defaultInterpreterPath"] = python_path - # Set interpreterPath as well for better compatibility with different editor versions - workspace_config["settings"]["python.interpreterPath"] = python_path - - # Add extra paths for better autocompletion - extra_paths = [p.as_posix() for p in process.addons_paths if p.exists()] - workspace_config["settings"]["python.analysis.extraPaths"] = extra_paths - workspace_config["settings"]["python.autoComplete.extraPaths"] = extra_paths - - # Force Ruff extension to use the binary from the venv - ruff_bin = (process.venv.path / "bin" / "ruff").as_posix() - workspace_config["settings"]["ruff.path"] = [ruff_bin] - workspace_config["settings"]["ruff.importStrategy"] = "fromEnvironment" - - for worktree in process.odoo_worktrees: - worktree_path = {"path": worktree.path.as_posix()} - if worktree_path not in workspace_config["folders"]: - workspace_config["folders"].append(worktree_path) - - self.workspace_path.write_text(json.dumps(workspace_config, indent=4)) - return True + with open(self.workspace_path, "w", encoding="utf-8") as f: + f.write(rendered_template) def _create_launch(self): """Create a launch file for the project.""" - process = ( - self.database.process - if isinstance(self.database, LocalDatabase) and self.database.process - else OdoobinProcess(self.database, version=self.version).with_edition("enterprise") - ) - - def run_config(shell: bool = False): - title = "Shell" if shell else "Run" - return { - "name": title, - "type": "debugpy", - "request": "launch", - "subProcess": True, - "justMyCode": True, - "console": "integratedTerminal", - "consoleName": f"Odev {title} ({self.database.name})", - "cwd": self.path.as_posix(), - "program": self.database.odev.executable.as_posix(), - "python": process.venv.python.as_posix(), - "args": [ - title.lower(), - self.database.name, - "--log-handler=odoo.addons.base.models.ir_attachment:WARNING", - "--limit-time-cpu=0", - "--limit-time-real=0", - ], - } - - launch_config = { - "version": "0.2.0", - "configurations": [ - run_config(), - run_config(True), - { - "name": "Attach Debugger", - "type": "debugpy", - "request": "attach", - "processId": "${command:pickProcess}", - }, - ], - } - - self.launch_path.write_text(json.dumps(launch_config, indent=4)) + rendered_template = self._get_rendered_template("launch.jinja") + with open(self.launch_path, "w", encoding="utf-8") as f: + f.write(rendered_template) def _create_tasks(self): """Create a tasks file for the project.""" - - tasks_config = { - "version": "2.0.0", - "tasks": [], + rendered_template = self._get_rendered_template( + "tasks.jinja", + DB_VERSION=self.version, + ) + with open(self.tasks_path, "w", encoding="utf-8") as f: + f.write(rendered_template) + + def _create_jsconfig(self): + """Create JS config file to provide intellisense JavaScript.""" + root = self.odoo_path.resolve() + + addon_dirs = [ + root / "addons", + root / "odoo" / "addons", + root / "enterprise", + self.path, + ] + + paths_map = { + "@odoo/owl": ["odoo/addons/web/static/src/@types/owl.d.ts"], + "@odoo/hoot": ["odoo/addons/web/static/src/@types/hoot.d.ts"], + "@odoo/hoot-dom": ["odoo/addons/web/static/src/@types/hoot.d.ts"], } - self.tasks_path.write_text(json.dumps(tasks_config, indent=4)) + for addon_dir in addon_dirs: + if not addon_dir.exists(): + continue + for module in addon_dir.iterdir(): + if module.is_dir(): + static_src_path = module / "static" / "src" + if static_src_path.exists(): + rel_path = os.path.relpath(static_src_path, root) + paths_map[f"@{module.name}/*"] = [f"{rel_path}/*"] + + modules_mapping = dict(sorted(paths_map.items())) + + rendered_template = self._get_rendered_template( + "jsconfig.jinja", + ODOO_PATH=self.odoo_path, + JS_MODULES_PATHS=json.dumps(modules_mapping, indent=4), + ) + with open(self.path / "jsconfig.json", "w", encoding="utf-8") as f: + f.write(rendered_template) diff --git a/config.py b/config.py new file mode 100644 index 0000000..c4968db --- /dev/null +++ b/config.py @@ -0,0 +1,20 @@ +from odev.common.config import Section + + +class VscodeSection(Section): + _name = "vscode" + + @property + def workspace_layout(self) -> str: + """Layout of the multi-root VSCode workspace, one of: + - 'flat': One top-level root per odoo worktree alongside the project (legacy default). + - 'nested': A single 'odoo' root holding all worktrees as subfolders. + Defaults to 'flat'. + """ + return self.get("workspace_layout", "flat") + + @workspace_layout.setter + def workspace_layout(self, value: str): + if value not in ("flat", "nested"): + raise ValueError(f"'vscode.workspace_layout' must be one of 'flat', 'nested', got {value!r}") + self.set("workspace_layout", value) diff --git a/templates/code-workspace.jinja b/templates/code-workspace.jinja new file mode 100644 index 0000000..f98f5ec --- /dev/null +++ b/templates/code-workspace.jinja @@ -0,0 +1,50 @@ +{ + "folders": {{ FOLDERS }}, + "settings": { + "odoo.db": "{{ DB_NAME }}", + "odoo.modules": "{{ DB_NAME }}_suite", + "odoo.testTags": "*", + "odoo.logLevel": "info", + + "python.languageServer": "None", + "python.analysis.typeCheckingMode": "standard", + "python.analysis.extraPaths": [ + "{{ ODOO_PATH }}/odoo", + "{{ ODOO_PATH }}/enterprise", + ], + "python.analysis.exclude": [ + "{{ ODOO_PATH }}", + ], + "python.analysis.packageIndexDepths": [ + { + "name": "odoo", + "depth": 2 + } + ], + "python.autoComplete.extraPaths": [ + "{{ ODOO_PATH }}/odoo", + "{{ ODOO_PATH }}/enterprise", + ], + "python.defaultInterpreterPath": "{{ VENV_PATH }}", + "python.interpreterPath": "{{ VENV_PATH }}", + + "ruff.path": ["{{ RUFF_PATH }}"], + "ruff.importStrategy": "fromEnvironment", + + "editor.formatOnSave": false, + "editor.rulers": [ + 120, + ], + + "search.exclude": { + "**/*.po*": true, + "**/LICENSE": true, + "**/README.md": true, + }, + + "terminal.integrated.cwd": "${workspaceFolder:project}", + + "pythonPath": "{{ PYTHON_PATH }}", + "odevPath": "{{ ODEV_EXE_PATH }}", + } +} diff --git a/templates/jsconfig.jinja b/templates/jsconfig.jinja new file mode 100644 index 0000000..e0aaab5 --- /dev/null +++ b/templates/jsconfig.jinja @@ -0,0 +1,41 @@ +{ + "include": [ + "**/*.js", + "**/*.d.ts", + "{{ ODOO_PATH }}/**/static/**/*.js", + "{{ ODOO_PATH }}/**/static/**/*.d.ts" + ], + "exclude": [ + "**/node_modules", + "**/setup", + "**/doc", + "**/lib/ace", + "**/lib/bootstrap", + "**/lib/Chart", + "**/lib/chartjs-adapter-luxon", + "**/lib/fullcalendar", + "**/lib/jquery", + "**/lib/luxon", + "**/lib/odoo_ui_icons", + "**/lib/pdfjs", + "**/lib/popper", + "**/lib/qunit", + "**/lib/signature_pad", + "**/lib/stacktracejs", + "**/lib/zxing-library", + "**/l10n*", + "**/o_spreadsheet.js" + ], + "compilerOptions": { + "moduleResolution": "node", + + "baseUrl": "{{ ODOO_PATH }}", + + "target": "ES2022", + "noEmit": true, + "disableSizeLimit": true, + + "paths": + {{ JS_MODULES_PATHS }} + } +} diff --git a/templates/launch.jinja b/templates/launch.jinja new file mode 100644 index 0000000..6e3a218 --- /dev/null +++ b/templates/launch.jinja @@ -0,0 +1,125 @@ +{ + "configurations": [ + { + "name": "Run", + "type": "debugpy", + "request": "launch", + "subProcess": true, + "justMyCode": true, + "program": "${config:odevPath}", + "cwd": "${workspaceFolder:project}", + "python": "${config:pythonPath}", + "args": [ + "run", + "${config:odoo.db}", + "--log-level=${config:odoo.logLevel}", + "--dev=xml", + "--update=${config:odoo.modules}", + "--log-handler=odoo.addons.base.models.ir_attachment:WARNING", + "--limit-time-cpu=0", + "--limit-time-real=0", + "--max-cron-threads=1", + ] + }, + { + "name": "Run without odev", + "type": "debugpy", + "request": "launch", + "program": "./odoo/odoo-bin", + "cwd": "${workspaceFolder:odoo}", + "args": [ + "--dev=xml", + "--database=${config:odoo.db}", + "--update=${config:odoo.modules}", + "--addons-path=odoo/addons,enterprise,design-themes,${workspaceFolder:project}", + "--log-level=${config:odoo.logLevel}", + "--limit-time-cpu=0", + "--limit-time-real=0", + "--max-cron-threads=1", + ] + }, + { + "name": "Test", + "type": "debugpy", + "request": "launch", + "subProcess": true, + "justMyCode": true, + "program": "${config:odevPath}", + "cwd": "${workspaceFolder:project}", + "python": "${config:pythonPath}", + "args": [ + "test", + "-v", + "${config:odoo.logLevel}", + "-t", + "${config:odoo.testTags}", + "-i", + "${config:odoo.modules}", + "-f", + "${config:odoo.db}", + "-p", + "8068", + ] + }, + { + "name": "Test without odev", + "type": "debugpy", + "request": "launch", + "program": "./odoo/odoo-bin", + "cwd": "${workspaceFolder:odoo}", + "preLaunchTask": "update modules (test db)", + "args": [ + "--test-tags=${config:odoo.testTags}", + "--database=${config:odoo.db}_test", + "--addons-path=./odoo/addons,./enterprise,${workspaceFolder:project}", + "--log-level=${config:odoo.logLevel}", + "--limit-time-cpu=0", + "--limit-time-real=0", + "--stop-after-init", + "--max-cron-threads=0", + "-p", + "8067", + ] + }, + { + "name": "Shell", + "type": "debugpy", + "request": "launch", + "subProcess": true, + "justMyCode": true, + "program": "${config:odevPath}", + "cwd": "${workspaceFolder:project}", + "python": "${config:pythonPath}", + "args": [ + "shell", + "${config:odoo.db}", + "--update=${config:odoo.modules}", + "--log-level=${config:odoo.logLevel}", + "--dev=xml", + "--limit-time-cpu=0", + "--limit-time-real=0", + "--max-cron-threads=0", + "--no-http", + ] + }, + { + "name": "Attach Remote", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + }, + "pathMappings": [ + { + "remoteRoot": "/home/odoo/src/user", + "localRoot": "${workspaceFolder:project}", + }, + { + "remoteRoot": "/home/odoo/src", + "localRoot": "${workspaceFolder:odoo}", + } + ], + }, + ] +} diff --git a/templates/tasks.jinja b/templates/tasks.jinja new file mode 100644 index 0000000..bd12f06 --- /dev/null +++ b/templates/tasks.jinja @@ -0,0 +1,207 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "~Init Fresh DB", + "dependsOn": ["create fresh db"], + "problemMatcher": [], + "detail": "Creates a new database and initializes modules.", + "presentation": { + "panel": "shared" + } + }, + { + "label": "~Init Fresh Test DB", + "dependsOn": ["create fresh test db"], + "problemMatcher": [], + "detail": "Creates a new test database and initializes modules.", + "presentation": { + "panel": "shared" + } + }, + { + "label": "~Init DB From Template", + "dependsOrder": "sequence", + "dependsOn": [ + "copy db from template", + "init modules (main db)" + ], + "problemMatcher": [], + "detail": "Restores database from template, then initializes modules.", + "presentation": { + "panel": "shared" + } + }, + { + "label": "~Init Test DB From Template", + "dependsOrder": "sequence", + "dependsOn": [ + "copy test db from template", + "init modules (test db)" + ], + "problemMatcher": [], + "detail": "Restores test database from template, then initializes modules.", + "presentation": { + "panel": "shared" + } + }, + { + "label": "~Create Template From DB", + "dependsOn": ["create template from db"], + "problemMatcher": [], + "detail": "Creates a template from the main database.", + "presentation": { + "panel": "shared" + } + }, + + { + "label": "create fresh db", + "command": "odev", + "problemMatcher": [], + "args": [ + "create", + "-f", + "-V", + "{{ DB_VERSION }}", + "${config:odoo.db}", + "--init=${config:odoo.modules}", + "--log-level=${config:odoo.logLevel}", + "--no-http", + ], + "presentation": { + "panel": "shared" + }, + }, + { + "label": "create fresh test db", + "command": "odev", + "problemMatcher": [], + "args": [ + "create", + "-f", + "-V", + "{{ DB_VERSION }}", + "${config:odoo.db}_test", + "--init=${config:odoo.modules}", + "--log-level=${config:odoo.logLevel}", + "--no-http", + ], + "presentation": { + "panel": "shared" + }, + }, + { + "label": "create template from db", + "command": "odev", + "problemMatcher": [], + "args": [ + "create", + "-T", + "-f", + "${config:odoo.db}", + ], + "presentation": { + "panel": "shared", + "reveal": "silent" + }, + }, + { + "label": "copy db from template", + "command": "odev", + "problemMatcher": [], + "args": [ + "create", + "-t", + "${config:odoo.db}:template", + "-f", + "${config:odoo.db}", + ], + "presentation": { + "panel": "shared" + }, + }, + { + "label": "copy test db from template", + "command": "odev", + "problemMatcher": [], + "args": [ + "create", + "-t", + "${config:odoo.db}:template", + "-f", + "${config:odoo.db}_test", + ], + "presentation": { + "panel": "shared" + }, + }, + { + "label": "init modules (main db)", + "command": "odev", + "problemMatcher": [], + "args": [ + "run", + "${config:odoo.db}", + "--init=${config:odoo.modules}", + "--log-level=${config:odoo.logLevel}", + "--no-http", + "--stop-after-init", + ], + "presentation": { + "panel": "shared" + }, + }, + { + "label": "init modules (test db)", + "command": "odev", + "problemMatcher": [], + "args": [ + "run", + "${config:odoo.db}_test", + "--init=${config:odoo.modules}", + "--log-level=${config:odoo.logLevel}", + "--no-http", + "--stop-after-init", + ], + "presentation": { + "panel": "shared" + }, + }, + { + "label": "update modules (test db)", + "command": "odev", + "problemMatcher": [], + "args": [ + "run", + "${config:odoo.db}_test", + "--update=${config:odoo.modules}", + "--log-level=${config:odoo.logLevel}", + "--no-http", + "--stop-after-init", + ], + "presentation": { + "panel": "shared" + }, + }, + { + "label": "~SSH Tunnel for Debugging", + "type": "shell", + "command": "HOST=\"${input:sshTarget}\"; HOST=\"${HOST#ssh }\"; ssh $HOST \"grep -q 'debugpy.listen' /home/odoo/src/odoo/odoo/addons/base/__init__.py || printf '\\ntry:\\n import debugpy\\n debugpy.listen((\\\"127.0.0.1\\\", 5678))\\nexcept:\\n pass\\n' >> /home/odoo/src/odoo/odoo/addons/base/__init__.py; odoosh-restart http\" && ssh -t -L 5678:127.0.0.1:5678 $HOST", + "problemMatcher": [], + "detail": "Open an SSH tunnel for remote debugging.", + "presentation": { + "reveal": "always", + "panel": "new" + } + } + ], + "inputs": [ + { + "id": "sshTarget", + "type": "promptString", + "description": "SSH Command or Host (e.g. ssh user@hostname or just user@hostname)", + "default": "odoo@localhost" + } + ] +}