From 418e7060f9d735d038a0ed263e83af85f6d8b3ca Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Sat, 13 Sep 2025 23:02:21 +1000 Subject: [PATCH 1/8] Add Open in Editor support for template paths --- debug_toolbar/panels/templates/panel.py | 3 +++ debug_toolbar/settings.py | 1 + .../debug_toolbar/panels/templates.html | 10 +++++++- debug_toolbar/utils.py | 25 +++++++++++++++++++ docs/configuration.rst | 10 ++++++++ example/settings.py | 4 ++- 6 files changed, 51 insertions(+), 2 deletions(-) diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py index 6dbd02ee0..fb9985d49 100644 --- a/debug_toolbar/panels/templates/panel.py +++ b/debug_toolbar/panels/templates/panel.py @@ -16,6 +16,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 @@ -195,6 +196,7 @@ def generate_stats(self, request, response): if hasattr(template, "origin") and template.origin and template.origin.name: template.origin_name = template.origin.name template.origin_hash = signing.dumps(template.origin.name) + template.editor_url = get_editor_url(template.origin.name) else: template.origin_name = _("No origin") template.origin_hash = "" @@ -202,6 +204,7 @@ def generate_stats(self, request, response): "name": template.name, "origin_name": template.origin_name, "origin_hash": template.origin_hash, + "editor_url": getattr(template, "editor_url", None), } # Clean up context for better readability if self.toolbar.config["SHOW_TEMPLATE_CONTEXT"]: 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..ab7f73a93 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/templates.html +++ b/debug_toolbar/templates/debug_toolbar/panels/templates.html @@ -15,7 +15,15 @@

{% 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 }} + {% else %} + {{ template.template.origin_name|addslashes }} + {% endif %} + +
{% if template.context %}
diff --git a/debug_toolbar/utils.py b/debug_toolbar/utils.py index f4b3eac38..fb2970ba6 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/configuration.rst b/docs/configuration.rst index 46359da83..88dd0c78c 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -67,6 +67,16 @@ 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: ``'vscode'``, ``'cursor'``, ``'emacs'``, ``'idea'``, + ``'pycharm'``, ``'sublime'``, ``'vscode-insiders'``, ``'vscode-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", + } From bda95cffce476777c83e14b6d354b3eec4bd49ad Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Tue, 7 Oct 2025 23:23:58 +0900 Subject: [PATCH 2/8] Run editor_url logic when retrieving stats --- debug_toolbar/panels/templates/panel.py | 12 +++++++++--- .../templates/debug_toolbar/panels/templates.html | 4 +++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py index fb9985d49..04a209aac 100644 --- a/debug_toolbar/panels/templates/panel.py +++ b/debug_toolbar/panels/templates/panel.py @@ -196,15 +196,13 @@ def generate_stats(self, request, response): if hasattr(template, "origin") and template.origin and template.origin.name: template.origin_name = template.origin.name template.origin_hash = signing.dumps(template.origin.name) - template.editor_url = get_editor_url(template.origin.name) else: - template.origin_name = _("No origin") + template.origin_name = None template.origin_hash = "" info["template"] = { "name": template.name, "origin_name": template.origin_name, "origin_hash": template.origin_hash, - "editor_url": getattr(template, "editor_url", None), } # Clean up context for better readability if self.toolbar.config["SHOW_TEMPLATE_CONTEXT"]: @@ -241,3 +239,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: + template["template"]["editor_url"] = get_editor_url(origin_name) + return stats diff --git a/debug_toolbar/templates/debug_toolbar/panels/templates.html b/debug_toolbar/templates/debug_toolbar/panels/templates.html index ab7f73a93..3f8d3e9f5 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/templates.html +++ b/debug_toolbar/templates/debug_toolbar/panels/templates.html @@ -19,8 +19,10 @@

{% blocktranslate count template_count=templates|length %}Template{% plural {% if template.template.editor_url %} {{ template.template.origin_name|addslashes }} - {% else %} + {% elif template.template.origin_name %} {{ template.template.origin_name|addslashes }} + {% else %} + {% translate "No origin" %} {% endif %}

From 8dab973afda9f972a7da47f8d07cf22e9b321646 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Wed, 8 Oct 2025 08:25:13 +0900 Subject: [PATCH 3/8] Add tests for get_editor_url --- debug_toolbar/utils.py | 4 ++-- tests/test_utils.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/debug_toolbar/utils.py b/debug_toolbar/utils.py index fb2970ba6..526d703f7 100644 --- a/debug_toolbar/utils.py +++ b/debug_toolbar/utils.py @@ -409,11 +409,11 @@ def get_editor_url(file: str, line: int = 1) -> str | None: "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();})()", + "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();})()", + "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}", diff --git a/tests/test_utils.py b/tests/test_utils.py index 646b6a5ad..42f1dda75 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,33 @@ 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", + } + 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) From 7efb4593e5bfa80ff07efd55056ee21ffe9831e8 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Wed, 8 Oct 2025 08:35:20 +0900 Subject: [PATCH 4/8] Add tests for open editor url to templates panel --- debug_toolbar/panels/templates/panel.py | 3 ++- tests/panels/test_template.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py index 04a209aac..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 @@ -244,6 +245,6 @@ def get_stats(self): stats = super().get_stats() for template in stats.get("templates", []): origin_name = template["template"]["origin_name"] - if origin_name: + if origin_name and origin_name != UNKNOWN_SOURCE: template["template"]["editor_url"] = get_editor_url(origin_name) return stats diff --git a/tests/panels/test_template.py b/tests/panels/test_template.py index f79914024..0d60e0755 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,18 @@ def test_template_source(self): response = self.client.get(url, data) self.assertEqual(response.status_code, 200) + def test_get_stats(self): + response = self.panel.process_request(self.request) + Template("").render(Context({})) + Template("", origin=Origin("test.html")).render(Context({})) + self.panel.generate_stats(self.request, response) + stats = self.panel.get_stats() + self.assertNotIn("editor_url", stats["templates"][0]["template"]) + self.assertEqual( + stats["templates"][1]["template"]["editor_url"], + "vscode://file/test.html:1", + ) + @override_settings( DEBUG=True, DEBUG_TOOLBAR_PANELS=["debug_toolbar.panels.templates.TemplatesPanel"] From be11fba374e86540f3609559fd5b50027690c792 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Wed, 8 Oct 2025 08:39:29 +0900 Subject: [PATCH 5/8] Improve EDITOR format in docs --- docs/configuration.rst | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 88dd0c78c..b2cebdcc3 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -73,9 +73,23 @@ Toolbar options The editor to use to open file paths from the toolbar. - Available editors: ``'vscode'``, ``'cursor'``, ``'emacs'``, ``'idea'``, - ``'pycharm'``, ``'sublime'``, ``'vscode-insiders'``, ``'vscode-remote'``, - ``'vscodium'``, ``'windsurf'`` + 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`` From d0eedfc063fab1cfcd64ea1603f5666544038631 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Thu, 9 Oct 2025 12:03:26 +0900 Subject: [PATCH 6/8] Add non-existent editor case to get_editor_url test --- tests/test_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 42f1dda75..f7534a95f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -194,6 +194,7 @@ def test_get_editor_url(self): "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 ( From dfef5a60517cda9a1f1aaa8217b619fb8b4c81f6 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Thu, 9 Oct 2025 12:05:33 +0900 Subject: [PATCH 7/8] Split TemplatePanel get_stats test cases --- tests/panels/test_template.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/panels/test_template.py b/tests/panels/test_template.py index 0d60e0755..a23957428 100644 --- a/tests/panels/test_template.py +++ b/tests/panels/test_template.py @@ -153,18 +153,23 @@ def test_template_source(self): response = self.client.get(url, data) self.assertEqual(response.status_code, 200) - def test_get_stats(self): + def test_get_stats_includes_editor_url(self): response = self.panel.process_request(self.request) - Template("").render(Context({})) Template("", origin=Origin("test.html")).render(Context({})) self.panel.generate_stats(self.request, response) stats = self.panel.get_stats() - self.assertNotIn("editor_url", stats["templates"][0]["template"]) self.assertEqual( - stats["templates"][1]["template"]["editor_url"], + 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"] From 943037dc7bc2a1cebdeb7379d4ba73287eb8c7f5 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Thu, 9 Oct 2025 12:09:01 +0900 Subject: [PATCH 8/8] Update changelog --- docs/changes.rst | 2 ++ 1 file changed, 2 insertions(+) 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) ------------------