diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py index 6dbd02ee0..8daaa8698 100644 --- a/debug_toolbar/panels/templates/panel.py +++ b/debug_toolbar/panels/templates/panel.py @@ -7,6 +7,7 @@ from django.core import signing from django.db.models.query import QuerySet, RawQuerySet from django.template import RequestContext, Template +from django.template.base import UNKNOWN_SOURCE from django.test.signals import template_rendered from django.test.utils import instrumented_test_render from django.urls import path @@ -16,6 +17,7 @@ from debug_toolbar.panels import Panel from debug_toolbar.panels.sql.tracking import SQLQueryTriggered, allow_sql from debug_toolbar.panels.templates import views +from debug_toolbar.utils import get_editor_url if find_spec("jinja2"): from debug_toolbar.panels.templates.jinja2 import patch_jinja_render @@ -196,7 +198,7 @@ def generate_stats(self, request, response): template.origin_name = template.origin.name template.origin_hash = signing.dumps(template.origin.name) else: - template.origin_name = _("No origin") + template.origin_name = None template.origin_hash = "" info["template"] = { "name": template.name, @@ -238,3 +240,11 @@ def generate_stats(self, request, response): "context_processors": context_processors, } ) + + def get_stats(self): + stats = super().get_stats() + for template in stats.get("templates", []): + origin_name = template["template"]["origin_name"] + if origin_name and origin_name != UNKNOWN_SOURCE: + template["template"]["editor_url"] = get_editor_url(origin_name) + return stats diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py index d6b9003b6..eb058351d 100644 --- a/debug_toolbar/settings.py +++ b/debug_toolbar/settings.py @@ -22,6 +22,7 @@ def _is_running_tests(): "debug_toolbar.panels.profiling.ProfilingPanel", "debug_toolbar.panels.redirects.RedirectsPanel", }, + "EDITOR": "vscode", "INSERT_BEFORE": "", "RENDER_PANELS": None, "RESULTS_CACHE_SIZE": 25, diff --git a/debug_toolbar/templates/debug_toolbar/panels/templates.html b/debug_toolbar/templates/debug_toolbar/panels/templates.html index 4ceae12e7..3f8d3e9f5 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/templates.html +++ b/debug_toolbar/templates/debug_toolbar/panels/templates.html @@ -15,7 +15,17 @@

{% blocktranslate count template_count=templates|length %}Template{% plural
{% for template in templates %}
{{ template.template.name|addslashes }}
-
{{ template.template.origin_name|addslashes }}
+
+ + {% if template.template.editor_url %} + {{ template.template.origin_name|addslashes }} + {% elif template.template.origin_name %} + {{ template.template.origin_name|addslashes }} + {% else %} + {% translate "No origin" %} + {% endif %} + +
{% if template.context %}
diff --git a/debug_toolbar/utils.py b/debug_toolbar/utils.py index f4b3eac38..526d703f7 100644 --- a/debug_toolbar/utils.py +++ b/debug_toolbar/utils.py @@ -401,3 +401,28 @@ def is_processable_html_response(response): and content_encoding == "" and content_type in _HTML_TYPES ) + + +def get_editor_url(file: str, line: int = 1) -> str | None: + formats = { + "cursor": "cursor://file/{file}:{line}", + "emacs": "emacs://open?url=file://{file}&line={line}", + "espresso": "x-espresso://open?filepath={file}&lines={line}", + "idea": "idea://open?file={file}&line={line}", + "idea-remote": "javascript:(()=>{{let r=new XMLHttpRequest; r.open('get','http://localhost:63342/api/file/?file={file}&line={line}');r.send();}})()", + "macvim": "mvim://open/?url=file://{file}&line={line}", + "nova": "nova://open?path={file}&line={line}", + "pycharm": "pycharm://open?file={file}&line={line}", + "pycharm-remote": "javascript:(()=>{{let r=new XMLHttpRequest; r.open('get','http://localhost:63342/api/file/{file}:{line}');r.send();}})()", + "sublime": "subl://open?url=file://{file}&line={line}", + "vscode": "vscode://file/{file}:{line}", + "vscode-insiders": "vscode-insiders://file/{file}:{line}", + "vscode-remote": "vscode://vscode-remote/{file}:{line}", + "vscode-insiders-remote": "vscode-insiders://vscode-remote/{file}:{line}", + "vscodium": "vscodium://file/{file}:{line}", + "windsurf": "windsurf://file/{file}:{line}", + } + template = formats.get(dt_settings.get_config()["EDITOR"]) + if template is None: + return None + return template.format(file=file, line=line) diff --git a/docs/changes.rst b/docs/changes.rst index 452242279..bd17bc8f4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -63,6 +63,8 @@ Pending conflicts * Added CSS for resetting the height of elements too to avoid problems with global CSS of a website where the toolbar is used. +* Added open in editor functionality to templates panel using ``EDITOR`` + setting. 5.1.0 (2025-03-20) ------------------ diff --git a/docs/configuration.rst b/docs/configuration.rst index 46359da83..b2cebdcc3 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -67,6 +67,30 @@ Toolbar options This setting is a set of the full Python paths to each panel that you want disabled (but still displayed) by default. +* ``EDITOR`` + + Default: ``'vscode'`` + + The editor to use to open file paths from the toolbar. + + Available editors: + + * ``'cursor'`` + * ``'emacs'`` + * ``'idea'`` + * ``'idea-remote'`` + * ``'macvim'`` + * ``'nova'`` + * ``'pycharm'`` + * ``'pycharm-remote'`` + * ``'sublime'`` + * ``'vscode'`` + * ``'vscode-insiders'`` + * ``'vscode-remote'`` + * ``'vscode-insiders-remote'`` + * ``'vscodium'`` + * ``'windsurf'`` + * ``INSERT_BEFORE`` Default: ``''`` diff --git a/example/settings.py b/example/settings.py index ffaa09fe5..4d0dfc274 100644 --- a/example/settings.py +++ b/example/settings.py @@ -118,4 +118,6 @@ "debug_toolbar.middleware.DebugToolbarMiddleware", ] # Customize the config to support turbo and htmx boosting. - DEBUG_TOOLBAR_CONFIG = {"ROOT_TAG_EXTRA_ATTRS": "data-turbo-permanent hx-preserve"} + DEBUG_TOOLBAR_CONFIG = { + "ROOT_TAG_EXTRA_ATTRS": "data-turbo-permanent hx-preserve", + } diff --git a/tests/panels/test_template.py b/tests/panels/test_template.py index f79914024..a23957428 100644 --- a/tests/panels/test_template.py +++ b/tests/panels/test_template.py @@ -2,7 +2,7 @@ import django from django.contrib.auth.models import User -from django.template import Context, RequestContext, Template +from django.template import Context, Origin, RequestContext, Template from django.test import override_settings from django.utils.functional import SimpleLazyObject @@ -153,6 +153,23 @@ def test_template_source(self): response = self.client.get(url, data) self.assertEqual(response.status_code, 200) + def test_get_stats_includes_editor_url(self): + response = self.panel.process_request(self.request) + Template("", origin=Origin("test.html")).render(Context({})) + self.panel.generate_stats(self.request, response) + stats = self.panel.get_stats() + self.assertEqual( + stats["templates"][0]["template"]["editor_url"], + "vscode://file/test.html:1", + ) + + def test_get_stats_excludes_editor_url(self): + response = self.panel.process_request(self.request) + Template("").render(Context({})) + self.panel.generate_stats(self.request, response) + stats = self.panel.get_stats() + self.assertNotIn("editor_url", stats["templates"][0]["template"]) + @override_settings( DEBUG=True, DEBUG_TOOLBAR_PANELS=["debug_toolbar.panels.templates.TemplatesPanel"] diff --git a/tests/test_utils.py b/tests/test_utils.py index 646b6a5ad..f7534a95f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,6 +5,7 @@ import debug_toolbar.utils from debug_toolbar.utils import ( + get_editor_url, get_name_from_obj, get_stack, get_stack_trace, @@ -171,3 +172,34 @@ def test_non_dict_input(self): test_input = ["not", "a", "dict"] result = sanitize_and_sort_request_vars(test_input) self.assertEqual(result["raw"], test_input) + + +class GetEditorUrlTestCase(unittest.TestCase): + @override_settings(DEBUG_TOOLBAR_CONFIG={"EDITOR": "vscode"}) + def test_get_editor_url(self): + editors = { + "cursor": "cursor://file/test.py:5", + "emacs": "emacs://open?url=file://test.py&line=5", + "espresso": "x-espresso://open?filepath=test.py&lines=5", + "idea": "idea://open?file=test.py&line=5", + "idea-remote": "javascript:(()=>{let r=new XMLHttpRequest; r.open('get','http://localhost:63342/api/file/?file=test.py&line=5');r.send();})()", + "macvim": "mvim://open/?url=file://test.py&line=5", + "nova": "nova://open?path=test.py&line=5", + "pycharm": "pycharm://open?file=test.py&line=5", + "pycharm-remote": "javascript:(()=>{let r=new XMLHttpRequest; r.open('get','http://localhost:63342/api/file/test.py:5');r.send();})()", + "sublime": "subl://open?url=file://test.py&line=5", + "vscode": "vscode://file/test.py:5", + "vscode-insiders": "vscode-insiders://file/test.py:5", + "vscode-remote": "vscode://vscode-remote/test.py:5", + "vscode-insiders-remote": "vscode-insiders://vscode-remote/test.py:5", + "vscodium": "vscodium://file/test.py:5", + "windsurf": "windsurf://file/test.py:5", + "non-existent-editor": None, + } + for editor, expected_url in editors.items(): + with ( + self.subTest(editor=editor), + override_settings(DEBUG_TOOLBAR_CONFIG={"EDITOR": editor}), + ): + url = get_editor_url("test.py", 5) + self.assertEqual(url, expected_url)