diff --git a/.gitignore b/.gitignore
index bc429f201b3e..68ec0e77b294 100644
--- a/.gitignore
+++ b/.gitignore
@@ -82,6 +82,7 @@ _build
# No benchmarks output
/benchmarks/
benchmark_*.json
+/pyrightconfig.json
# Ruff cache
diff --git a/.vscode/tools/settings.template.json b/.vscode/tools/settings.template.json
index 4b07a6a8f9ad..e9ade2972128 100644
--- a/.vscode/tools/settings.template.json
+++ b/.vscode/tools/settings.template.json
@@ -78,8 +78,5 @@
},
"[restructuredtext]": {
"editor.tabSize": 2
- },
- // Python extra paths
- // Note: this is filled up when "./isaaclab.sh -i" is run
- "python.analysis.extraPaths": []
+ }
}
diff --git a/.vscode/tools/setup_vscode.py b/.vscode/tools/setup_vscode.py
index 8d29dafee080..42f5507e2f68 100644
--- a/.vscode/tools/setup_vscode.py
+++ b/.vscode/tools/setup_vscode.py
@@ -3,197 +3,75 @@
#
# SPDX-License-Identifier: BSD-3-Clause
-"""This script sets up the vs-code settings for the Isaac Lab project.
+"""Set up VS Code and Cursor for an Isaac Lab repository or external project.
-This script merges the python.analysis.extraPaths from the "{ISAACSIM_DIR}/.vscode/settings.json" file into
-the ".vscode/settings.json" file.
-
-This is necessary because Isaac Sim 2022.2.1 onwards does not add the necessary python packages to the python path
-when the "setup_python_env.sh" is run as part of the vs-code launch configuration.
+The script writes editor settings and a machine-local ``pyrightconfig.json``. The Pyright
+configuration extends the project's checked-in ``pyproject.toml`` and adds import roots
+discovered from the active Python environment. This supports source, editable, wheel, and
+Isaac Sim binaries installations without storing machine-specific paths in version control.
"""
+import argparse
+import pathlib
import re
-import subprocess
import sys
-import os
-import pathlib
-
-
-ISAACLAB_DIR = pathlib.Path(__file__).parents[2]
-"""Path to the Isaac Lab directory."""
-
-# Try to find IsaacSim dir
-_isaacsim_probe = subprocess.run(
- [sys.executable, "-c", "import isaacsim; import os; print(os.environ.get('ISAAC_PATH', ''))"],
- capture_output=True,
- text=True,
- check=False,
- # avoid EULA prompt
- stdin=subprocess.DEVNULL,
-)
-if _isaacsim_probe.returncode == 0 and _isaacsim_probe.stdout.strip():
- isaacsim_dir = _isaacsim_probe.stdout.strip()
-else:
- isaacsim_dir = os.path.join(ISAACLAB_DIR, "_isaac_sim")
-
-# check if the isaac-sim directory exists
-if not os.path.exists(isaacsim_dir):
- print(
- f"[WARN] Could not find the isaac-sim directory: {isaacsim_dir}."
- "\n\tIsaac Sim does not appear to be installed. VS Code settings will be generated"
- "\n\twithout Isaac Sim extra paths."
- )
- isaacsim_dir = ""
-
-ISAACSIM_DIR = isaacsim_dir
-"""Path to the isaac-sim directory."""
-
-
-def overwrite_python_analysis_extra_paths(isaaclab_settings: str) -> str:
- """Overwrite the python.analysis.extraPaths in the Isaac Lab settings file.
- The extraPaths are replaced with the path names from the isaac-sim settings file that exists in the
- "{ISAACSIM_DIR}/.vscode/settings.json" file.
+from isaaclab.utils.editor import build_extra_paths, resolve_isaacsim_dir, write_pyright_config
- If the isaac-sim settings file does not exist, the extraPaths are not overwritten.
+PROJECT_DIR = pathlib.Path(__file__).parents[2]
+"""Path to the repository or generated project's root directory."""
- Args:
- isaaclab_settings: The settings string to use as template.
- Returns:
- The settings string with overwritten python analysis extra paths.
- """
- # isaac-sim settings
- isaacsim_vscode_filename = os.path.join(ISAACSIM_DIR, ".vscode", "settings.json")
-
- # we use the isaac-sim settings file to get the python.analysis.extraPaths for kit extensions
- # if this file does not exist, we will not add any extra paths
- if ISAACSIM_DIR and os.path.exists(isaacsim_vscode_filename):
- # read the path names from the isaac-sim settings file
- with open(isaacsim_vscode_filename) as f:
- vscode_settings = f.read()
- # extract the path names
- # search for the python.analysis.extraPaths section and extract the contents
- settings = re.search(
- r"\"python.analysis.extraPaths\": \[.*?\]", vscode_settings, flags=re.MULTILINE | re.DOTALL
- )
- settings = settings.group(0)
- settings = settings.split('"python.analysis.extraPaths": [')[-1]
- settings = settings.split("]")[0]
-
- # read the path names from the isaac-sim settings file
- path_names = settings.split(",")
- path_names = [path_name.strip().strip('"') for path_name in path_names]
- path_names = [path_name for path_name in path_names if len(path_name) > 0]
-
- # change the path names to be relative to the Isaac Lab directory
- rel_path = os.path.relpath(ISAACSIM_DIR, ISAACLAB_DIR)
- path_names = ['"${workspaceFolder}/' + rel_path + "/" + path_name + '"' for path_name in path_names]
- else:
- path_names = []
-
- # add the path names that are in the Isaac Lab extensions directory
- isaaclab_extensions = os.listdir(os.path.join(ISAACLAB_DIR, "source"))
- path_names.extend(['"${workspaceFolder}/source/' + ext + '"' for ext in isaaclab_extensions])
-
- # combine them into a single string
- path_names = ",\n\t\t".expandtabs(4).join(path_names)
- # deal with the path separator being different on Windows and Unix
- path_names = path_names.replace("\\", "/")
-
- # replace the path names in the Isaac Lab settings file with the path names parsed
- isaaclab_settings = re.sub(
- r"\"python.analysis.extraPaths\": \[.*?\]",
- '"python.analysis.extraPaths": [\n\t\t'.expandtabs(4) + path_names + "\n\t]".expandtabs(4),
- isaaclab_settings,
- flags=re.DOTALL,
- )
- # return the Isaac Lab settings string
- return isaaclab_settings
-
-
-def overwrite_default_python_interpreter(isaaclab_settings: str) -> str:
- """Overwrite the default python interpreter in the Isaac Lab settings file.
-
- The default python interpreter is replaced with the path to the python interpreter used by the
- isaac-sim project. This is necessary because the default python interpreter is the one shipped with
- isaac-sim.
+def overwrite_default_python_interpreter(settings: str, isaacsim_dir: pathlib.Path | None) -> str:
+ """Set the editor's default Python interpreter.
Args:
- isaaclab_settings: The settings string to use as template.
+ settings: VS Code settings template.
+ isaacsim_dir: Isaac Sim installation directory, or None.
Returns:
- The settings string with overwritten default python interpreter.
+ Settings with the interpreter path updated.
"""
- # read executable name
- python_exe = sys.executable.replace("\\", "/")
-
- # We make an exception for replacing the default interpreter if the
- # path (/kit/python/bin/python3) indicates that we are using a local/container
- # installation of IsaacSim. We will preserve the calling script as the default, python.sh.
- # We want to use python.sh because it modifies LD_LIBRARY_PATH and PYTHONPATH
- # (among other envars) that we need for all of our dependencies to be accessible.
- if "kit/python/bin/python3" in python_exe:
- return isaaclab_settings
- # replace the default python interpreter in the Isaac Lab settings file with the path to the
- # python interpreter in the Isaac Lab directory
- isaaclab_settings = re.sub(
- r"\"python.defaultInterpreterPath\": \".*?\"",
- f'"python.defaultInterpreterPath": "{python_exe}"',
- isaaclab_settings,
+ python_exe = pathlib.Path(sys.executable)
+ if "kit/python/bin/python3" in python_exe.as_posix() and isaacsim_dir is not None:
+ wrapper = isaacsim_dir / "python.sh"
+ if wrapper.is_file():
+ python_exe = wrapper
+ return re.sub(
+ r'"python\.defaultInterpreterPath": ".*?"',
+ f'"python.defaultInterpreterPath": "{python_exe.as_posix()}"',
+ settings,
flags=re.DOTALL,
)
- # return the Isaac Lab settings file
- return isaaclab_settings
def main():
- # Isaac Lab template settings
- isaaclab_vscode_template_filename = os.path.join(ISAACLAB_DIR, ".vscode", "tools", "settings.template.json")
- # make sure the Isaac Lab template settings file exists
- if not os.path.exists(isaaclab_vscode_template_filename):
- raise FileNotFoundError(
- f"Could not find the Isaac Lab template settings file: {isaaclab_vscode_template_filename}"
- )
- # read the Isaac Lab template settings file
- with open(isaaclab_vscode_template_filename) as f:
- isaaclab_template_settings = f.read()
-
- # overwrite the python.analysis.extraPaths in the Isaac Lab settings file with the path names
- isaaclab_settings = overwrite_python_analysis_extra_paths(isaaclab_template_settings)
- # overwrite the default python interpreter in the Isaac Lab settings file with the path to the
- # python interpreter used to call this script
- isaaclab_settings = overwrite_default_python_interpreter(isaaclab_settings)
-
- # add template notice to the top of the file
- header_message = (
- "// This file is a template and is automatically generated by the setup_vscode.py script.\n"
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--isaac_path", help="Absolute path to the Isaac Sim installation.")
+ args = parser.parse_args()
+
+ try:
+ isaacsim_dir = resolve_isaacsim_dir(PROJECT_DIR, args.isaac_path)
+ except ValueError as error:
+ parser.error(str(error))
+ write_pyright_config(PROJECT_DIR, build_extra_paths(PROJECT_DIR, isaacsim_dir))
+
+ settings_template = PROJECT_DIR / ".vscode" / "tools" / "settings.template.json"
+ if not settings_template.is_file():
+ raise FileNotFoundError(f"Could not find the VS Code settings template: {settings_template}")
+ settings = overwrite_default_python_interpreter(settings_template.read_text(encoding="utf-8"), isaacsim_dir)
+ header = (
+ "// This file is automatically generated by setup_vscode.py.\n"
"// Do not edit this file directly.\n"
- "// \n"
- f"// Generated from: {isaaclab_vscode_template_filename}\n"
+ f"// Generated from: {settings_template}\n"
)
- isaaclab_settings = header_message + isaaclab_settings
-
- # write the Isaac Lab settings file
- isaaclab_vscode_filename = os.path.join(ISAACLAB_DIR, ".vscode", "settings.json")
- with open(isaaclab_vscode_filename, "w") as f:
- f.write(isaaclab_settings)
-
- # copy the launch.json file if it doesn't exist
- isaaclab_vscode_launch_filename = os.path.join(ISAACLAB_DIR, ".vscode", "launch.json")
- isaaclab_vscode_template_launch_filename = os.path.join(ISAACLAB_DIR, ".vscode", "tools", "launch.template.json")
- if not os.path.exists(isaaclab_vscode_launch_filename):
- # read template launch settings
- with open(isaaclab_vscode_template_launch_filename) as f:
- isaaclab_template_launch_settings = f.read()
- # add header
- header_message = header_message.replace(
- isaaclab_vscode_template_filename, isaaclab_vscode_template_launch_filename
- )
- isaaclab_launch_settings = header_message + isaaclab_template_launch_settings
- # write the Isaac Lab launch settings file
- with open(isaaclab_vscode_launch_filename, "w") as f:
- f.write(isaaclab_launch_settings)
+ (PROJECT_DIR / ".vscode" / "settings.json").write_text(header + settings, encoding="utf-8")
+
+ launch_file = PROJECT_DIR / ".vscode" / "launch.json"
+ launch_template = PROJECT_DIR / ".vscode" / "tools" / "launch.template.json"
+ if not launch_file.exists():
+ launch_header = header.replace(str(settings_template), str(launch_template))
+ launch_file.write_text(launch_header + launch_template.read_text(encoding="utf-8"), encoding="utf-8")
if __name__ == "__main__":
diff --git a/docs/source/developer-tools/template_generator.rst b/docs/source/developer-tools/template_generator.rst
index c69288396663..80fb06e50439 100644
--- a/docs/source/developer-tools/template_generator.rst
+++ b/docs/source/developer-tools/template_generator.rst
@@ -242,12 +242,28 @@ External projects should build their environment harness from public APIs and
maintain project-local fixtures. Copying ``env_test_utils.py`` into a project is
vendoring it, so the project must track upstream changes to that copy.
-To configure VS Code, run the generated setup task or invoke it directly:
+To configure VS Code or Cursor, run the generated setup task or invoke it directly:
.. code-block:: bash
uv run python .vscode/tools/setup_vscode.py
+The command selects the active interpreter and creates a git-ignored
+``pyrightconfig.json``. This child configuration inherits the checked-in
+Pyright policy from ``pyproject.toml`` and adds the generated project's
+``src`` import root, installed Isaac Lab packages, and any discovered Isaac
+Sim extensions. When using the ``isaacsim`` extra, include it while generating
+the configuration:
+
+.. code-block:: bash
+
+ uv run --extra isaacsim python .vscode/tools/setup_vscode.py
+
+In VS Code, use Pylance and select the interpreter that ran the setup command.
+In Cursor, install the ``detachhead.basedpyright`` extension instead of Pylance,
+select the same interpreter, and reload the window. Both language servers read
+the generated ``pyrightconfig.json``.
+
Create an internal task
-----------------------
diff --git a/docs/source/overview/developer-guide/vs_code.rst b/docs/source/overview/developer-guide/vs_code.rst
index 23ea18956bdc..330d5d9a0714 100644
--- a/docs/source/overview/developer-guide/vs_code.rst
+++ b/docs/source/overview/developer-guide/vs_code.rst
@@ -1,11 +1,15 @@
.. _setup-vs-code:
-Setting up Visual Studio Code
------------------------------
+Setting up VS Code or Cursor
+----------------------------
-**This is optional. You do not need to use VScode to use Isaac Lab**
+Editor setup is optional and is not required to run Isaac Lab. The repository
+includes shared settings for `Visual Studio Code `_
+and compatible editors such as `Cursor `_. Complete one
+of the :ref:`Isaac Lab installation methods ` before
+configuring your editor.
-`Visual Studio Code `_ has proven an invaluable tool for the development of Isaac Lab. The Isaac Lab repository includes the VSCode files for setting up your development environment. These are included in the ``.vscode`` directory and include the following files:
+The ``.vscode`` directory contains the checked-in templates and tasks:
.. code-block:: bash
@@ -15,42 +19,123 @@ Setting up Visual Studio Code
│ ├── settings.template.json
│ └── setup_vscode.py
├── extensions.json
- ├── launch.json # <- this is generated by setup_vscode.py
- ├── settings.json # <- this is generated by setup_vscode.py
+ ├── launch.json # generated by setup_vscode.py
+ ├── settings.json # generated by setup_vscode.py
└── tasks.json
+Configure a source checkout
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
-.. attention::
+Open the Isaac Lab repository root in your editor. Then run the setup command
+that matches your installation from a terminal in that directory.
- The following instructions on setting up Visual Studio Code only work with
- :ref:`Isaac Sim Binaries Installation ` and not with
- :ref:`Python Environment with Isaac Sim `.
+.. tab-set::
+ .. tab-item:: uv (recommended)
-To setup the IDE, please follow these instructions:
+ For the default Newton environment, run:
-1. Open the ``IsaacLab`` directory on Visual Studio Code IDE
-2. Run VSCode `Tasks `__, by
- pressing ``Ctrl+Shift+P``, selecting ``Tasks: Run Task`` and running the
- ``setup_python_env`` in the drop down menu.
+ .. code-block:: bash
- .. image:: ../../_static/vscode_tasks.png
- :width: 600px
- :align: center
- :alt: VSCode Tasks
+ uv run python .vscode/tools/setup_vscode.py
+ If you use Isaac Sim from the ``isaacsim`` extra, include the extra so the
+ setup command can discover its extensions:
-.. note::
- If this is your first time running tasks in VS Code, you may be prompted to select how to handle warnings. Simply follow
- the prompts until the task window closes.
+ .. code-block:: bash
-If everything executes correctly, it should create the following files:
+ uv run --extra isaacsim python .vscode/tools/setup_vscode.py
-* ``.vscode/launch.json``: Contains the launch configurations for debugging python code.
-* ``.vscode/settings.json``: Contains the settings for the python interpreter and the python environment.
+ .. tab-item:: Activated Python environment
-For more information on VSCode support for Omniverse, please refer to the
-following links:
+ Activate the uv, venv, or conda environment where Isaac Lab is installed,
+ then run:
+
+ .. code-block:: bash
+
+ python .vscode/tools/setup_vscode.py
+
+ .. tab-item:: Downloaded Isaac Sim package
+
+ Run the setup script through the Isaac Lab launcher after completing the
+ :ref:`downloaded package installation `:
+
+ .. tab-set::
+ :sync-group: os
+
+ .. tab-item:: :icon:`fa-brands fa-linux` Linux
+ :sync: linux
+
+ .. code-block:: bash
+
+ ./isaaclab.sh -p .vscode/tools/setup_vscode.py
+
+ .. tab-item:: :icon:`fa-brands fa-windows` Windows
+ :sync: windows
+
+ .. code-block:: batch
+
+ isaaclab.bat -p .vscode\tools\setup_vscode.py
+
+ The ``setup_python_env`` task in the command palette runs the same launcher
+ workflow for a downloaded package.
+
+The command creates or updates these machine-local files:
+
+* ``.vscode/launch.json``: Debugging configurations. An existing file is preserved.
+* ``.vscode/settings.json``: The interpreter and shared editor settings.
+* ``pyrightconfig.json``: Import paths for Pyright-compatible language servers.
+
+The generated files are ignored by Git because interpreter and extension paths
+vary between machines. Rerun the command after changing Python environments or
+Isaac Sim installations. If Isaac Sim is not installed, the command prints a
+warning and still configures the local Isaac Lab packages.
+
+The checked-in ``[tool.pyright]`` table in ``pyproject.toml`` makes packages
+under ``source`` available immediately after cloning. The generated
+``pyrightconfig.json`` inherits that policy and adds Isaac Sim extensions plus
+Isaac Lab packages found in the active Python environment. This covers source,
+editable, and wheel installations without storing absolute paths in Git.
+
+Configure VS Code
+^^^^^^^^^^^^^^^^^
+
+Install the extensions recommended by the repository when VS Code prompts you.
+At minimum, install the Python and Pylance extensions. Run the setup command
+above, then use **Python: Select Interpreter** from the command palette to select
+the same interpreter used by the command. For the recommended uv installation,
+this is ``.venv/bin/python`` on Linux or ``.venv\Scripts\python.exe`` on Windows.
+
+Configure Cursor
+^^^^^^^^^^^^^^^^
+
+Cursor cannot use Pylance because Pylance is licensed for official VS Code
+builds. Install the Python extension and the `basedpyright
+`__
+extension (``detachhead.basedpyright``). Then:
+
+1. Run the same setup command shown above for your installation.
+2. Select the interpreter that ran the command.
+3. Reload the Cursor window so basedpyright rereads ``pyrightconfig.json``.
+
+No Cursor-specific path list is required. Pylance and basedpyright read the
+same Pyright configuration.
+
+Troubleshoot editor imports
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If an import is still unresolved:
+
+1. Confirm the selected editor interpreter matches ``python`` in the setup command.
+2. Rerun setup with ``--extra isaacsim`` if the missing import is from
+ ``omni``, ``pxr``, or ``isaacsim``.
+3. Reload the editor window.
+4. Inspect the generated ``extraPaths`` in the root ``pyrightconfig.json``.
+
+Remove simulator extension directories that the project does not use if
+language-server indexing consumes too much memory.
+
+For more information about VS Code support in Isaac Sim, see:
* `Isaac Sim VSCode support `__
@@ -84,26 +169,24 @@ To use it:
and press the green play button or ``F5``. VS Code will connect to the debugpy server
running on ``localhost:3000``.
-Configuring the python interpreter
+Configuring the Python interpreter
----------------------------------
-In the provided configuration, we set the default python interpreter to use the
-python executable provided by Omniverse. This is specified in the
-``.vscode/settings.json`` file:
+The setup command records the interpreter that ran it in
+``.vscode/settings.json``. For example, a uv source checkout on Linux uses:
.. code-block:: json
{
- "python.defaultInterpreterPath": "${workspaceFolder}/_isaac_sim/python.sh",
+ "python.defaultInterpreterPath": "/path/to/IsaacLab/.venv/bin/python",
}
-If you want to use a different python interpreter (for instance, from your conda or uv environment),
-you need to change the python interpreter used by selecting and activating the python interpreter
-of your choice in the bottom left corner of VSCode, or opening the command palette (``Ctrl+Shift+P``)
-and selecting ``Python: Select Interpreter``.
+The editor selection takes precedence over this default. If you change
+environments, rerun setup and select the new interpreter from the status bar or
+with **Python: Select Interpreter** in the command palette.
-For more information on how to set python interpreter for VSCode, please
-refer to the `VSCode documentation `_.
+For more information about selecting a Python interpreter, see the
+`VS Code documentation `_.
Setting up formatting and linting
diff --git a/docs/source/setup/installation/index.rst b/docs/source/setup/installation/index.rst
index c71865eb4086..b86c4313265b 100644
--- a/docs/source/setup/installation/index.rst
+++ b/docs/source/setup/installation/index.rst
@@ -771,7 +771,7 @@ The first launch downloads Isaac Sim extensions and can take more than ten minut
you to accept the NVIDIA Omniverse EULA; set ``OMNI_KIT_ACCEPT_EULA=yes`` for a non-interactive
environment. Run a project script with ``python my_script.py``.
-Generate VS Code settings for the current workspace with:
+Generate VS Code or Cursor settings for the current workspace with:
.. code-block:: bash
@@ -779,8 +779,10 @@ Generate VS Code settings for the current workspace with:
.. warning::
- This command generates ``.vscode/settings.json`` in the workspace. If the file already exists,
- it asks before overwriting it.
+ This command generates ``.vscode/settings.json`` and ``pyrightconfig.json`` in the workspace.
+ The Pyright configuration inherits an existing ``[tool.pyright]`` table and adds paths discovered
+ from the active Python environment. If ``.vscode/settings.json`` already exists, the command asks
+ before overwriting it.
.. _installation-method-binary:
.. _isaaclab-binaries-installation:
diff --git a/docs/source/setup/quickstart.rst b/docs/source/setup/quickstart.rst
index 2934b2097e35..4270c6e48727 100644
--- a/docs/source/setup/quickstart.rst
+++ b/docs/source/setup/quickstart.rst
@@ -36,6 +36,26 @@ Training outputs, including checkpoints, are saved under ``logs/``. Add
uv run isaaclab train --help
+Configure an editor (optional)
+------------------------------
+
+To enable import completion and debugging in VS Code or Cursor, generate the
+machine-local editor configuration from the repository root:
+
+.. code-block:: bash
+
+ uv run python .vscode/tools/setup_vscode.py
+
+If you use the ``isaacsim`` extra, include it so the command can discover the
+Isaac Sim extensions:
+
+.. code-block:: bash
+
+ uv run --extra isaacsim python .vscode/tools/setup_vscode.py
+
+VS Code uses Pylance. Cursor users should install basedpyright instead. See
+:ref:`setup-vs-code` for complete editor and troubleshooting instructions.
+
.. The quickstart media is generated by tools/docs/media/generate_quickstart.sh.
.. figure:: https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/quickstart_task_categories.gif
diff --git a/pyproject.toml b/pyproject.toml
index 78d2120635d6..6437b464ce79 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -325,6 +325,11 @@ exclude = [
".vscode",
]
+# Make the in-repo packages resolvable by the language server (Pylance / basedpyright)
+# without an editable install, so imports like ``isaaclab.assets`` work out of the box.
+# The glob keeps new source packages discoverable without maintaining a duplicate package list.
+extraPaths = ["source/*"]
+
typeCheckingMode = "basic"
pythonVersion = "3.12"
pythonPlatform = "Linux"
diff --git a/source/isaaclab/changelog.d/mhaiderbhai-fix-pyright-editor-setup.rst b/source/isaaclab/changelog.d/mhaiderbhai-fix-pyright-editor-setup.rst
new file mode 100644
index 000000000000..3ed215622ff0
--- /dev/null
+++ b/source/isaaclab/changelog.d/mhaiderbhai-fix-pyright-editor-setup.rst
@@ -0,0 +1,4 @@
+Fixed
+^^^^^
+
+* Fixed VS Code and Cursor import resolution for source, editable, wheel, and Isaac Sim binaries installations.
diff --git a/source/isaaclab/isaaclab/__main__.py b/source/isaaclab/isaaclab/__main__.py
index 5ea66bbe64c3..781ba02b96c9 100644
--- a/source/isaaclab/isaaclab/__main__.py
+++ b/source/isaaclab/isaaclab/__main__.py
@@ -4,14 +4,10 @@
# SPDX-License-Identifier: BSD-3-Clause
import argparse
-import os
-import re
+import pathlib
import sys
-import textwrap
-import tomllib
-
-from isaaclab.paths import ISAACLAB_ROOT
+from isaaclab.utils.editor import build_extra_paths, resolve_isaacsim_dir, write_pyright_config
VSCODE_SETTINGS_TEMPLATE = """
{
@@ -21,11 +17,8 @@
"python.languageServer": "Pylance",
"python.jediEnabled": false,
- // Those paths are automatically filled by isaaclab (see: 'python -m isaaclab --help')
+ // This path is automatically filled by isaaclab
"python.defaultInterpreterPath": "PYTHON.DEFAULTINTERPRETERPATH",
- "python.analysis.extraPaths": [
- PYTHON.ANALYSIS.EXTRAPATHS
- ],
// Use "black" as a formatter
"python.formatting.provider": "black",
@@ -33,108 +26,38 @@
// Use "flake8" for linting
"python.linting.pylintEnabled": false,
- "python.linting.flake8Enabled": true,
+ "python.linting.flake8Enabled": true
}
"""
-def generate_vscode_settings():
- def _mock_python_modules(ext_path: str, ext_name: str) -> None:
- # parse config/extension.toml
- cprint(f" |-- Parsing extension config ({ext_path})")
- config_path = os.path.join(ext_path, "config", "extension.toml")
- try:
- with open(config_path, "rb") as f:
- config = tomllib.load(f)
- except Exception as e:
- cprint(f" | |-- [Warning] {e}")
- return
- # get python modules
- for item in config.get("python", {}).get("module", []):
- if list(item.keys()) == ["name"]:
- # skip tests
- if item.get("name", "").endswith(".tests"):
- continue
- # mock __init__.py for each submodule (if not exists)
- submodule_path = ext_path
- for submodule in item.get("name", "").split("."):
- init_path = os.path.join(submodule_path, "__init__.py")
- if not os.path.isfile(init_path):
- try:
- cprint(f" |-- Mocking {init_path}")
- with open(init_path, "w") as f:
- f.write("# Generated by 'isaaclab' package")
- except Exception as e:
- cprint(f" | |-- [Warning] {e}")
- continue
- submodule_path = os.path.join(submodule_path, submodule)
-
- def _get_paths(base_path: str, mock_python_modules: bool = False) -> list[str]:
- paths = []
- if os.path.isdir(base_path):
- for folder in os.listdir(base_path):
- folder_path = os.path.join(base_path, folder)
- if os.path.isdir(folder_path):
- paths.append(folder_path)
- cprint(f"Registering extension: {folder_path}")
- if mock_python_modules:
- _mock_python_modules(folder_path, re.split(r"-\d+", folder)[0])
- return paths
-
- try:
- import omni.kit_app # importing 'omni.kit_app' will bootstrap kernel
-
- kit_path = os.path.dirname(os.path.abspath(os.path.realpath(omni.kit_app.__file__)))
- except ModuleNotFoundError:
- print("Unable to find 'omniverse-kit' package")
- # exit()
- try:
- import isaacsim
-
- isaacsim_path = os.path.dirname(os.path.abspath(os.path.realpath(isaacsim.__file__)))
- except ModuleNotFoundError:
- print("Unable to find 'isaacsim' package")
- # exit()
-
- cwd = os.getcwd()
- vscode_settings_path = os.path.join(cwd, ".vscode", "settings.json")
- # check if .vscode/settings.json exists
- if os.path.exists(vscode_settings_path):
+def generate_vscode_settings(isaac_path: str | None = None, verbose: bool = False):
+ """Generate editor settings and a Pyright configuration in the current workspace.
+
+ Args:
+ isaac_path: Explicit Isaac Sim installation path, or None to discover it.
+ verbose: Whether to print every generated Pyright search path.
+ """
+ project_dir = pathlib.Path.cwd()
+ vscode_settings_path = project_dir / ".vscode" / "settings.json"
+ if vscode_settings_path.exists():
print(f"VS Code settings already exists: {vscode_settings_path}")
if input("Overwrite? (y/N): ").lower() not in ["y", "yes"]:
print("Cancelled: VS Code settings not overwritten")
return
- # get extensions paths
- extensions_paths = []
- # - omniverse-kit
- folder_path = os.path.join(kit_path, "kernel", "py")
- if os.path.isdir(folder_path):
- extensions_paths.append(folder_path)
- for folder in ["exts", "extscore"]:
- extensions_paths.extend(_get_paths(os.path.join(kit_path, folder), mock_python_modules=True))
- # - isaacsim
- for folder in ["exts", "extscache", "extsDeprecated", "extsUser"]:
- extensions_paths.extend(_get_paths(os.path.join(isaacsim_path, folder), mock_python_modules=True))
- # - isaaclab
- isaaclab_path = str(ISAACLAB_ROOT)
- for folder in ["source"]:
- extensions_paths.extend(_get_paths(os.path.join(isaaclab_path, folder), mock_python_modules=True))
-
- # update 'python.defaultInterpreterPath'
- template = VSCODE_SETTINGS_TEMPLATE[:]
- template = template.replace("PYTHON.DEFAULTINTERPRETERPATH", sys.executable)
-
- # update 'python.analysis.extraPaths'
- content = "\n".join([f'"{path}",' for path in extensions_paths])
- content = textwrap.indent(content, prefix=" " * 8)[8:]
- template = template.replace("PYTHON.ANALYSIS.EXTRAPATHS", content)
-
- # create .vscode/settings.json
- os.makedirs(os.path.join(cwd, ".vscode"), exist_ok=True)
- with open(vscode_settings_path, "w") as f:
- f.write(template)
- print("VS Code settings generated at", vscode_settings_path)
+ isaacsim_dir = resolve_isaacsim_dir(project_dir, isaac_path)
+ extra_paths = build_extra_paths(project_dir, isaacsim_dir)
+ write_pyright_config(project_dir, extra_paths)
+ if verbose:
+ for path in extra_paths:
+ print(f"Registered Pyright search path: {path}")
+
+ settings = VSCODE_SETTINGS_TEMPLATE.replace("PYTHON.DEFAULTINTERPRETERPATH", pathlib.Path(sys.executable).as_posix())
+ vscode_settings_path.parent.mkdir(parents=True, exist_ok=True)
+ vscode_settings_path.write_text(settings, encoding="utf-8")
+ print(f"VS Code settings generated at {vscode_settings_path}")
+ print(f"Pyright configuration generated at {project_dir / 'pyrightconfig.json'}")
def main():
@@ -142,12 +65,16 @@ def main():
if len(sys.argv) > 1 and sys.argv[1] == "--generate-vscode-settings":
parser = argparse.ArgumentParser()
parser.add_argument("--generate-vscode-settings", action="store_true", help="Generate VS Code settings.")
+ parser.add_argument("--isaac_path", help="Absolute path to the Isaac Sim installation.")
parser.add_argument("--verbose", action="store_true", help="Print discovered extension paths.")
args = parser.parse_args()
-
- global cprint
- cprint = print if args.verbose else lambda *args, **kwargs: None
- generate_vscode_settings()
+ try:
+ if args.isaac_path or args.verbose:
+ generate_vscode_settings(isaac_path=args.isaac_path, verbose=args.verbose)
+ else:
+ generate_vscode_settings()
+ except ValueError as error:
+ parser.error(str(error))
return
from isaaclab.cli import cli
diff --git a/source/isaaclab/isaaclab/utils/editor.py b/source/isaaclab/isaaclab/utils/editor.py
new file mode 100644
index 000000000000..4272d0d012e9
--- /dev/null
+++ b/source/isaaclab/isaaclab/utils/editor.py
@@ -0,0 +1,156 @@
+# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Utilities for generating editor import paths for Isaac Lab projects."""
+
+import importlib.metadata
+import importlib.util
+import json
+import os
+import pathlib
+import re
+import subprocess
+import sys
+
+
+def resolve_isaacsim_dir(project_dir: pathlib.Path, isaac_path: str | None = None) -> pathlib.Path | None:
+ """Resolve the Isaac Sim installation directory.
+
+ Args:
+ project_dir: Project root containing an optional ``_isaac_sim`` link.
+ isaac_path: Explicit Isaac Sim path, or None to discover the installation.
+
+ Returns:
+ The resolved installation directory, or None if Isaac Sim is unavailable.
+
+ Raises:
+ ValueError: If an explicit path does not identify a directory.
+ """
+ if isaac_path:
+ explicit_path = pathlib.Path(isaac_path).expanduser()
+ if not _is_isaacsim_dir(explicit_path):
+ raise ValueError(f"Not an Isaac Sim directory (missing .vscode/settings.json): {explicit_path}")
+ return explicit_path.resolve()
+
+ env_path = os.environ.get("ISAAC_PATH")
+ if env_path and _is_isaacsim_dir(pathlib.Path(env_path)):
+ return pathlib.Path(env_path).resolve()
+
+ probe = subprocess.run(
+ [sys.executable, "-c", "import isaacsim, os; print(os.environ.get('ISAAC_PATH', ''))"],
+ capture_output=True,
+ text=True,
+ check=False,
+ stdin=subprocess.DEVNULL,
+ )
+ for line in reversed(probe.stdout.splitlines()):
+ candidate = pathlib.Path(line.strip()).expanduser()
+ if line.strip() and _is_isaacsim_dir(candidate):
+ return candidate.resolve()
+
+ fallback = project_dir / "_isaac_sim"
+ return fallback.resolve() if _is_isaacsim_dir(fallback) else None
+
+
+def read_isaacsim_extra_paths(isaacsim_dir: pathlib.Path | None) -> list[pathlib.Path]:
+ """Read Isaac Sim's Python extension paths.
+
+ Args:
+ isaacsim_dir: Isaac Sim installation directory, or None.
+
+ Returns:
+ Absolute extension search paths.
+ """
+ if isaacsim_dir is None:
+ print("[WARN] Isaac Sim was not found; simulator extension paths were not added.")
+ return []
+
+ settings_file = isaacsim_dir / ".vscode" / "settings.json"
+ if not settings_file.is_file():
+ print(f"[WARN] Isaac Sim VS Code settings were not found: {settings_file}")
+ return []
+
+ settings = settings_file.read_text(encoding="utf-8")
+ match = re.search(r'"python\.analysis\.extraPaths"\s*:\s*\[(.*?)\]', settings, flags=re.DOTALL)
+ if match is None:
+ print(f"[WARN] python.analysis.extraPaths was not found in {settings_file}")
+ return []
+
+ paths = []
+ for encoded_path in re.findall(r'"((?:\\.|[^"\\])*)"', match.group(1)):
+ path = pathlib.Path(json.loads(f'"{encoded_path}"'))
+ paths.append(path if path.is_absolute() else isaacsim_dir / path)
+ return paths
+
+
+def find_isaaclab_package_paths() -> list[pathlib.Path]:
+ """Find Isaac Lab package roots visible to the active interpreter.
+
+ Returns:
+ Import roots for installed Isaac Lab packages.
+ """
+ paths = []
+ package_names = sorted(name for name in importlib.metadata.packages_distributions() if name.startswith("isaaclab"))
+ for package_name in package_names:
+ spec = importlib.util.find_spec(package_name)
+ if spec is None:
+ continue
+ if spec.submodule_search_locations:
+ paths.extend(pathlib.Path(location).parent for location in spec.submodule_search_locations)
+ elif spec.origin:
+ paths.append(pathlib.Path(spec.origin).parent)
+ return paths
+
+
+def build_extra_paths(project_dir: pathlib.Path, isaacsim_dir: pathlib.Path | None) -> list[str]:
+ """Build Pyright search paths for simulator, local, and installed packages.
+
+ Args:
+ project_dir: Project root used to discover local ``source/*`` or ``src`` packages.
+ isaacsim_dir: Isaac Sim installation directory, or None.
+
+ Returns:
+ Deduplicated paths, relative to the project where practical.
+ """
+ paths = read_isaacsim_extra_paths(isaacsim_dir)
+ source_dir = project_dir / "source"
+ if source_dir.is_dir():
+ paths.extend(path for path in sorted(source_dir.iterdir()) if path.is_dir())
+ src_dir = project_dir / "src"
+ if src_dir.is_dir():
+ paths.append(src_dir)
+ paths.extend(find_isaaclab_package_paths())
+
+ formatted_paths = []
+ seen = set()
+ resolved_project_dir = project_dir.resolve()
+ for path in paths:
+ resolved_path = path.resolve()
+ try:
+ formatted_path = resolved_path.relative_to(resolved_project_dir).as_posix()
+ except ValueError:
+ formatted_path = resolved_path.as_posix()
+ if formatted_path not in seen:
+ seen.add(formatted_path)
+ formatted_paths.append(formatted_path)
+ return formatted_paths
+
+
+def write_pyright_config(project_dir: pathlib.Path, extra_paths: list[str]):
+ """Write a machine-local Pyright configuration that preserves project policy.
+
+ Args:
+ project_dir: Project root where the configuration is written.
+ extra_paths: Additional import search paths.
+ """
+ config: dict[str, object] = {"extraPaths": extra_paths}
+ if (project_dir / "pyproject.toml").is_file():
+ config["extends"] = "./pyproject.toml"
+ (project_dir / "pyrightconfig.json").write_text(json.dumps(config, indent=4) + "\n", encoding="utf-8")
+
+
+def _is_isaacsim_dir(path: pathlib.Path) -> bool:
+ """Check whether a directory contains the settings used for extension discovery."""
+ return path.is_dir() and (path / ".vscode" / "settings.json").is_file()
diff --git a/source/isaaclab/test/cli/test_installed_workflow_entrypoints.py b/source/isaaclab/test/cli/test_installed_workflow_entrypoints.py
index 9a8951730e8f..04735d78beac 100644
--- a/source/isaaclab/test/cli/test_installed_workflow_entrypoints.py
+++ b/source/isaaclab/test/cli/test_installed_workflow_entrypoints.py
@@ -49,6 +49,21 @@ def test_legacy_vscode_option_uses_compatibility_dispatcher():
generate.assert_called_once_with()
+def test_installed_vscode_generator_uses_pyright_config(tmp_path, monkeypatch):
+ """The installed workflow must not emit the conflicting Pylance extraPaths setting."""
+ monkeypatch.chdir(tmp_path)
+ with (
+ mock.patch.object(package_main, "resolve_isaacsim_dir", return_value=None),
+ mock.patch.object(package_main, "build_extra_paths", return_value=["/sim/exts/example"]),
+ mock.patch.object(package_main, "write_pyright_config") as write_pyright_config,
+ ):
+ package_main.generate_vscode_settings()
+
+ settings = (tmp_path / ".vscode" / "settings.json").read_text()
+ assert "python.analysis.extraPaths" not in settings
+ write_pyright_config.assert_called_once_with(tmp_path, ["/sim/exts/example"])
+
+
@pytest.mark.parametrize(
("command", "runner"),
[
diff --git a/source/isaaclab_rl/changelog.d/mhaiderbhai-vscode-pyright-tests.skip b/source/isaaclab_rl/changelog.d/mhaiderbhai-vscode-pyright-tests.skip
new file mode 100644
index 000000000000..3259ba2642e0
--- /dev/null
+++ b/source/isaaclab_rl/changelog.d/mhaiderbhai-vscode-pyright-tests.skip
@@ -0,0 +1 @@
+The template regression suite covers editor configuration generation.
diff --git a/source/isaaclab_rl/test/test_template_generator.py b/source/isaaclab_rl/test/test_template_generator.py
index 65bc3d786ae1..5a7b938bc31e 100644
--- a/source/isaaclab_rl/test/test_template_generator.py
+++ b/source/isaaclab_rl/test/test_template_generator.py
@@ -7,6 +7,7 @@
import ast
import importlib.util
+import json
import pkgutil
import subprocess
import sys
@@ -17,6 +18,8 @@
import pytest
import tomllib
+from isaaclab.utils import editor as editor_utils
+
ROOT_DIR = Path(__file__).resolve().parents[3]
TEMPLATE_TOOL_DIR = ROOT_DIR / "tools" / "template"
sys.path.insert(0, str(TEMPLATE_TOOL_DIR))
@@ -302,11 +305,63 @@ def test_external_project_uses_src_layout_and_installed_isaaclab_commands(tmp_pa
"build-backend": "uv_build",
}
assert package["project"]["dependencies"] == ["isaaclab[rsl-rl,skrl]"]
+ assert package["tool"]["pyright"] == {
+ "include": ["src", "scripts", "tests"],
+ "exclude": ["**/__pycache__", "**/logs", ".git", ".venv", ".vscode"],
+ "typeCheckingMode": "basic",
+ }
assert package["project"]["entry-points"]["isaaclab.tasks"] == {project_name: f"{project_name}.tasks"}
assert package["tool"]["uv"]["build-backend"]["module-name"] == project_name
assert (project_dir / "src" / project_name / "__init__.py").is_file()
assert not (project_dir / "source").exists()
assert {path.name for path in (project_dir / "scripts").iterdir()} == {"list_envs.py"}
+ assert "pyrightconfig.json" in (project_dir / ".gitignore").read_text()
+ assert (
+ "python.analysis.extraPaths" not in (project_dir / ".vscode" / "tools" / "settings.template.json").read_text()
+ )
+ assert (project_dir / ".vscode" / "tools" / "setup_vscode.py").read_bytes() == (
+ ROOT_DIR / ".vscode" / "tools" / "setup_vscode.py"
+ ).read_bytes()
+
+
+def test_editor_setup_combines_simulator_local_and_installed_paths(tmp_path, monkeypatch):
+ """The generated Pyright child config must preserve project policy and cover every installation mode."""
+ project_dir = tmp_path / "project"
+ (project_dir / "source" / "local_package").mkdir(parents=True)
+ (project_dir / "src" / "generated_project").mkdir(parents=True)
+ (project_dir / "pyproject.toml").write_text('[tool.pyright]\ntypeCheckingMode = "basic"\n')
+ isaacsim_dir = tmp_path / "isaacsim"
+ (isaacsim_dir / ".vscode").mkdir(parents=True)
+ (isaacsim_dir / ".vscode" / "settings.json").write_text(
+ '{"python.analysis.extraPaths": ["exts/isaacsim.core.api", "extscache/omni.kit.foo"]}'
+ )
+ installed_root = tmp_path / "editable" / "isaaclab"
+ installed_root.mkdir(parents=True)
+
+ monkeypatch.setattr(editor_utils, "find_isaaclab_package_paths", lambda: [installed_root])
+
+ extra_paths = editor_utils.build_extra_paths(project_dir, isaacsim_dir)
+ editor_utils.write_pyright_config(project_dir, extra_paths)
+ config = json.loads((project_dir / "pyrightconfig.json").read_text())
+
+ assert config["extends"] == "./pyproject.toml"
+ assert config["extraPaths"] == [
+ (isaacsim_dir / "exts" / "isaacsim.core.api").as_posix(),
+ (isaacsim_dir / "extscache" / "omni.kit.foo").as_posix(),
+ "source/local_package",
+ "src",
+ installed_root.as_posix(),
+ ]
+
+
+@pytest.mark.parametrize("path_exists", [False, True])
+def test_editor_setup_rejects_invalid_explicit_isaac_sim_path(tmp_path, path_exists):
+ """An invalid user-selected installation must not silently select a different Isaac Sim."""
+ invalid_path = tmp_path / "invalid"
+ if path_exists:
+ invalid_path.mkdir()
+ with pytest.raises(ValueError, match="Not an Isaac Sim directory"):
+ editor_utils.resolve_isaacsim_dir(tmp_path, str(invalid_path))
def _all_libraries() -> list[dict]:
diff --git a/tools/template/generator.py b/tools/template/generator.py
index eecb5381c6d8..c4aefcf6bff3 100644
--- a/tools/template/generator.py
+++ b/tools/template/generator.py
@@ -9,7 +9,7 @@
import subprocess
import jinja2
-from common import MULTI_AGENT_ALGORITHMS, SINGLE_AGENT_ALGORITHMS, TASKS_DIR, TEMPLATE_DIR
+from common import MULTI_AGENT_ALGORITHMS, ROOT_DIR, SINGLE_AGENT_ALGORITHMS, TASKS_DIR, TEMPLATE_DIR
jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(TEMPLATE_DIR),
@@ -199,6 +199,10 @@ def _external(specification: dict) -> None:
print(" |-- Copying vscode files...")
vscode_dir = os.path.join(project_dir, ".vscode")
shutil.copytree(os.path.join(TEMPLATE_DIR, "external", ".vscode"), vscode_dir, dirs_exist_ok=True)
+ shutil.copyfile(
+ os.path.join(ROOT_DIR, ".vscode", "tools", "setup_vscode.py"),
+ os.path.join(vscode_dir, "tools", "setup_vscode.py"),
+ )
template = jinja_env.get_template("external/.vscode/tasks.json")
_write_file(os.path.join(vscode_dir, "tasks.json"), content=template.render(**specification))
template = jinja_env.get_template("external/.vscode/tools/launch.template.json")
diff --git a/tools/template/templates/external/.gitignore b/tools/template/templates/external/.gitignore
index d7e3d459d3d1..d8f5bdf5325e 100644
--- a/tools/template/templates/external/.gitignore
+++ b/tools/template/templates/external/.gitignore
@@ -14,3 +14,4 @@ wandb/
.vscode/settings.json
.vscode/launch.json
+pyrightconfig.json
diff --git a/tools/template/templates/external/.vscode/tools/settings.template.json b/tools/template/templates/external/.vscode/tools/settings.template.json
index c1528d65dd73..d66c6f4f0322 100644
--- a/tools/template/templates/external/.vscode/tools/settings.template.json
+++ b/tools/template/templates/external/.vscode/tools/settings.template.json
@@ -72,8 +72,5 @@
},
"[restructuredtext]": {
"editor.tabSize": 2
- },
- // Python extra paths
- // Note: this is filled up when vscode is set up for the first time
- "python.analysis.extraPaths": []
+ }
}
diff --git a/tools/template/templates/external/.vscode/tools/setup_vscode.py b/tools/template/templates/external/.vscode/tools/setup_vscode.py
deleted file mode 100644
index 9fcd115748ec..000000000000
--- a/tools/template/templates/external/.vscode/tools/setup_vscode.py
+++ /dev/null
@@ -1,198 +0,0 @@
-# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
-# All rights reserved.
-#
-# SPDX-License-Identifier: BSD-3-Clause
-
-"""This script sets up the vs-code settings for the Isaac Lab project.
-
-This script merges the python.analysis.extraPaths from the "{ISAACSIM_DIR}/.vscode/settings.json" file into
-the ".vscode/settings.json" file.
-
-This is necessary because Isaac Sim 2022.2.1 onwards does not add the necessary python packages to the python path
-when the "setup_python_env.sh" is run as part of the vs-code launch configuration.
-"""
-
-import os
-import pathlib
-import re
-import subprocess
-import sys
-
-ISAACLAB_DIR = pathlib.Path(__file__).parents[2]
-"""Path to the Isaac Lab directory."""
-
-# Try to find IsaacSim dir
-_isaacsim_probe = subprocess.run(
- [sys.executable, "-c", "import isaacsim; import os; print(os.environ.get('ISAAC_PATH', ''))"],
- capture_output=True,
- text=True,
- check=False,
- # avoid EULA prompt
- stdin=subprocess.DEVNULL,
-)
-if _isaacsim_probe.returncode == 0 and _isaacsim_probe.stdout.strip():
- isaacsim_dir = _isaacsim_probe.stdout.strip()
-else:
- isaacsim_dir = os.path.join(ISAACLAB_DIR, "_isaac_sim")
-
-# check if the isaac-sim directory exists
-if not os.path.exists(isaacsim_dir):
- print(
- f"[WARN] Could not find the isaac-sim directory: {isaacsim_dir}."
- "\n\tIsaac Sim does not appear to be installed. VS Code settings will be generated"
- "\n\twithout Isaac Sim extra paths."
- )
- isaacsim_dir = ""
-
-ISAACSIM_DIR = isaacsim_dir
-"""Path to the isaac-sim directory."""
-
-
-def overwrite_python_analysis_extra_paths(isaaclab_settings: str) -> str:
- """Overwrite the python.analysis.extraPaths in the Isaac Lab settings file.
-
- The extraPaths are replaced with the path names from the isaac-sim settings file that exists in the
- "{ISAACSIM_DIR}/.vscode/settings.json" file.
-
- If the isaac-sim settings file does not exist, the extraPaths are not overwritten.
-
- Args:
- isaaclab_settings: The settings string to use as template.
-
- Returns:
- The settings string with overwritten python analysis extra paths.
- """
- # isaac-sim settings
- isaacsim_vscode_filename = os.path.join(ISAACSIM_DIR, ".vscode", "settings.json")
-
- # we use the isaac-sim settings file to get the python.analysis.extraPaths for kit extensions
- # if this file does not exist, we will not add any extra paths
- if ISAACSIM_DIR and os.path.exists(isaacsim_vscode_filename):
- # read the path names from the isaac-sim settings file
- with open(isaacsim_vscode_filename) as f:
- vscode_settings = f.read()
- # extract the path names
- # search for the python.analysis.extraPaths section and extract the contents
- settings = re.search(
- r"\"python.analysis.extraPaths\": \[.*?\]", vscode_settings, flags=re.MULTILINE | re.DOTALL
- )
- settings = settings.group(0)
- settings = settings.split('"python.analysis.extraPaths": [')[-1]
- settings = settings.split("]")[0]
-
- # read the path names from the isaac-sim settings file
- path_names = settings.split(",")
- path_names = [path_name.strip().strip('"') for path_name in path_names]
- path_names = [path_name for path_name in path_names if len(path_name) > 0]
-
- # change the path names to be relative to the Isaac Lab directory
- rel_path = os.path.relpath(ISAACSIM_DIR, ISAACLAB_DIR)
- path_names = ['"${workspaceFolder}/' + rel_path + "/" + path_name + '"' for path_name in path_names]
- else:
- path_names = []
-
- # add the generated project's source directory
- path_names.append('"${workspaceFolder}/src"')
-
- # combine them into a single string
- path_names = ",\n\t\t".expandtabs(4).join(path_names)
- # deal with the path separator being different on Windows and Unix
- path_names = path_names.replace("\\", "/")
-
- # replace the path names in the Isaac Lab settings file with the path names parsed
- isaaclab_settings = re.sub(
- r"\"python.analysis.extraPaths\": \[.*?\]",
- '"python.analysis.extraPaths": [\n\t\t'.expandtabs(4) + path_names + "\n\t]".expandtabs(4),
- isaaclab_settings,
- flags=re.DOTALL,
- )
- # return the Isaac Lab settings string
- return isaaclab_settings
-
-
-def overwrite_default_python_interpreter(isaaclab_settings: str) -> str:
- """Overwrite the default python interpreter in the Isaac Lab settings file.
-
- The default python interpreter is replaced with the path to the python interpreter used by the
- isaac-sim project. This is necessary because the default python interpreter is the one shipped with
- isaac-sim.
-
- Args:
- isaaclab_settings: The settings string to use as template.
-
- Returns:
- The settings string with overwritten default python interpreter.
- """
- # read executable name
- python_exe = sys.executable.replace("\\", "/")
-
- # We make an exception for replacing the default interpreter if the
- # path (/kit/python/bin/python3) indicates that we are using a local/container
- # installation of IsaacSim. We will preserve the calling script as the default, python.sh.
- # We want to use python.sh because it modifies LD_LIBRARY_PATH and PYTHONPATH
- # (among other envars) that we need for all of our dependencies to be accessible.
- if "kit/python/bin/python3" in python_exe:
- return isaaclab_settings
- # replace the default python interpreter in the Isaac Lab settings file with the path to the
- # python interpreter in the Isaac Lab directory
- isaaclab_settings = re.sub(
- r"\"python.defaultInterpreterPath\": \".*?\"",
- f'"python.defaultInterpreterPath": "{python_exe}"',
- isaaclab_settings,
- flags=re.DOTALL,
- )
- # return the Isaac Lab settings file
- return isaaclab_settings
-
-
-def main():
- # Isaac Lab template settings
- isaaclab_vscode_template_filename = os.path.join(ISAACLAB_DIR, ".vscode", "tools", "settings.template.json")
- # make sure the Isaac Lab template settings file exists
- if not os.path.exists(isaaclab_vscode_template_filename):
- raise FileNotFoundError(
- f"Could not find the Isaac Lab template settings file: {isaaclab_vscode_template_filename}"
- )
- # read the Isaac Lab template settings file
- with open(isaaclab_vscode_template_filename) as f:
- isaaclab_template_settings = f.read()
-
- # overwrite the python.analysis.extraPaths in the Isaac Lab settings file with the path names
- isaaclab_settings = overwrite_python_analysis_extra_paths(isaaclab_template_settings)
- # overwrite the default python interpreter in the Isaac Lab settings file with the path to the
- # python interpreter used to call this script
- isaaclab_settings = overwrite_default_python_interpreter(isaaclab_settings)
-
- # add template notice to the top of the file
- header_message = (
- "// This file is a template and is automatically generated by the setup_vscode.py script.\n"
- "// Do not edit this file directly.\n"
- "// \n"
- f"// Generated from: {isaaclab_vscode_template_filename}\n"
- )
- isaaclab_settings = header_message + isaaclab_settings
-
- # write the Isaac Lab settings file
- isaaclab_vscode_filename = os.path.join(ISAACLAB_DIR, ".vscode", "settings.json")
- with open(isaaclab_vscode_filename, "w") as f:
- f.write(isaaclab_settings)
-
- # copy the launch.json file if it doesn't exist
- isaaclab_vscode_launch_filename = os.path.join(ISAACLAB_DIR, ".vscode", "launch.json")
- isaaclab_vscode_template_launch_filename = os.path.join(ISAACLAB_DIR, ".vscode", "tools", "launch.template.json")
- if not os.path.exists(isaaclab_vscode_launch_filename):
- # read template launch settings
- with open(isaaclab_vscode_template_launch_filename) as f:
- isaaclab_template_launch_settings = f.read()
- # add header
- header_message = header_message.replace(
- isaaclab_vscode_template_filename, isaaclab_vscode_template_launch_filename
- )
- isaaclab_launch_settings = header_message + isaaclab_template_launch_settings
- # write the Isaac Lab launch settings file
- with open(isaaclab_vscode_launch_filename, "w") as f:
- f.write(isaaclab_launch_settings)
-
-
-if __name__ == "__main__":
- main()
diff --git a/tools/template/templates/external/README.md b/tools/template/templates/external/README.md
index 01d0c188b59b..44ec3f479c59 100644
--- a/tools/template/templates/external/README.md
+++ b/tools/template/templates/external/README.md
@@ -75,12 +75,39 @@ The test helpers under `source/isaaclab_tasks/test` in the Isaac Lab repository
`isaaclab_tasks` package. Keep test fixtures in this project and use public Isaac Lab APIs. If you copy
`env_test_utils.py`, it becomes vendored code whose upstream changes you must track.
-To configure VS Code, run the `setup_python_env` task or invoke its command directly:
+To configure VS Code or Cursor, run the `setup_python_env` task or invoke its command directly:
```bash
uv run python .vscode/tools/setup_vscode.py
```
+The setup command selects the active interpreter and generates a git-ignored `pyrightconfig.json`. The generated
+configuration inherits the project's checked-in Pyright settings and adds the Isaac Sim extensions, project `src` root,
+and any Isaac Lab packages discovered in the active Python environment. This supports both Pylance in VS Code and
+basedpyright in Cursor.
+
+In VS Code, use Pylance and select the interpreter that ran the setup command. In Cursor, install the
+[basedpyright extension](https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright) instead of
+Pylance, select the same interpreter, and reload the window. Both language servers read `pyrightconfig.json`.
+
+When using the `isaacsim` extra, include it while generating the editor configuration so the command can discover the
+Isaac Sim installation:
+
+```bash
+uv run --extra isaacsim python .vscode/tools/setup_vscode.py
+```
+
+For an Isaac Sim binaries installation that is not available in the project environment, run the setup with its Python
+launcher instead:
+
+```bash
+# Linux
+/python.sh .vscode/tools/setup_vscode.py --isaac_path
+
+# Windows
+\python.bat .vscode\tools\setup_vscode.py --isaac_path
+```
+
{% if include_ui_extension %}
## Isaac Sim UI extension
@@ -91,5 +118,6 @@ Add the project root to the Isaac Sim Extension Manager search paths, refresh, a
{% endif %}
## Troubleshooting
-If Pylance cannot resolve simulator modules, run the VS Code setup command above and reload the window. If indexing uses
-too much memory, remove unused simulator extension paths from `.vscode/settings.json`.
+If Pylance or basedpyright cannot resolve modules, confirm that the selected interpreter matches the one used to run the
+setup command, then reload the editor window. To add a missing extension or reduce indexing memory, edit the `extraPaths`
+array in the root `pyrightconfig.json`; remove simulator extension directories that the project does not use.
diff --git a/tools/template/templates/external/pyproject.toml b/tools/template/templates/external/pyproject.toml
index ae1b0ebb9d74..ff5a5acaa150 100644
--- a/tools/template/templates/external/pyproject.toml
+++ b/tools/template/templates/external/pyproject.toml
@@ -73,6 +73,11 @@ isaaclab = ["isaaclab", "isaaclab_newton", "isaaclab_ov", "isaaclab_physx"]
[tool.ruff.format]
docstring-code-format = true
+[tool.pyright]
+include = ["src", "scripts", "tests"]
+exclude = ["**/__pycache__", "**/logs", ".git", ".venv", ".vscode"]
+typeCheckingMode = "basic"
+
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra"