diff --git a/release/datafiles/userdef/userdef_default.c b/release/datafiles/userdef/userdef_default.c index b3a8ec5889d..f904603beee 100644 --- a/release/datafiles/userdef/userdef_default.c +++ b/release/datafiles/userdef/userdef_default.c @@ -38,6 +38,8 @@ const UserDef U_default = { .sounddir = "//", .i18ndir = "", .image_editor = "", + .text_editor = "", + .text_editor_args = "", .anim_player = "", .anim_player_preset = 0, .v2d_min_gridsize = 45, diff --git a/scripts/presets/text_editor/internal.py b/scripts/presets/text_editor/internal.py new file mode 100644 index 00000000000..9286fbf88fb --- /dev/null +++ b/scripts/presets/text_editor/internal.py @@ -0,0 +1,6 @@ +import bpy + +filepaths = bpy.context.preferences.filepaths + +filepaths.text_editor = "" +filepaths.text_editor_args = "" diff --git a/scripts/presets/text_editor/visual_studio_code.py b/scripts/presets/text_editor/visual_studio_code.py new file mode 100644 index 00000000000..b4261dc6987 --- /dev/null +++ b/scripts/presets/text_editor/visual_studio_code.py @@ -0,0 +1,12 @@ +import bpy +import platform + +filepaths = bpy.context.preferences.filepaths + +filepaths.text_editor_args = "-g $filepath:$line:$column" + +match platform.system(): + case "Windows": + filepaths.text_editor = "code.cmd" + case _: + filepaths.text_editor = "code" diff --git a/scripts/startup/bl_operators/__init__.py b/scripts/startup/bl_operators/__init__.py index de0b7798072..09bc3b8bf70 100644 --- a/scripts/startup/bl_operators/__init__.py +++ b/scripts/startup/bl_operators/__init__.py @@ -28,6 +28,7 @@ _modules = [ "screen_play_rendered_anim", "sequencer", "spreadsheet", + "text", "userpref", "uvcalc_follow_active", "uvcalc_lightmap", diff --git a/scripts/startup/bl_operators/presets.py b/scripts/startup/bl_operators/presets.py index 96a20e8d078..0a9cef8f43b 100644 --- a/scripts/startup/bl_operators/presets.py +++ b/scripts/startup/bl_operators/presets.py @@ -413,6 +413,24 @@ class AddPresetHairDynamics(AddPresetBase, Operator): ] +class AddPresetTextEditor(AddPresetBase, Operator): + """Add or remove a Text Editor Preset""" + bl_idname = "text_editor.preset_add" + bl_label = "Add Text Editor Preset" + preset_menu = "USERPREF_PT_text_editor_presets" + + preset_defines = [ + "filepaths = bpy.context.preferences.filepaths" + ] + + preset_values = [ + "filepaths.text_editor", + "filepaths.text_editor_args" + ] + + preset_subdir = "text_editor" + + class AddPresetTrackingCamera(AddPresetBase, Operator): """Add or remove a Tracking Camera Intrinsics Preset""" bl_idname = "clip.camera_preset_add" @@ -692,6 +710,7 @@ classes = ( AddPresetOperator, AddPresetRender, AddPresetCameraSafeAreas, + AddPresetTextEditor, AddPresetTrackingCamera, AddPresetTrackingSettings, AddPresetTrackingTrackColor, diff --git a/scripts/startup/bl_operators/text.py b/scripts/startup/bl_operators/text.py new file mode 100644 index 00000000000..03f537ffea0 --- /dev/null +++ b/scripts/startup/bl_operators/text.py @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +from bpy.types import Operator +from bpy.props import ( + IntProperty, + StringProperty, +) + + +class TEXT_OT_jump_to_file_at_point(Operator): + bl_idname = "text.jump_to_file_at_point" + bl_label = "Open Text File at point" + bl_description = "Edit text file in external text editor" + + filepath: StringProperty(name="filepath") + line: IntProperty(name="line") + column: IntProperty(name="column") + + def execute(self, context): + import shlex + import subprocess + from string import Template + + if not self.properties.is_property_set("filepath"): + text = context.space_data.text + if not text: + return {'CANCELLED'} + self.filepath = text.filepath + self.line = text.current_line_index + self.column = text.current_character + + text_editor = context.preferences.filepaths.text_editor + text_editor_args = context.preferences.filepaths.text_editor_args + + if not text_editor_args: + self.report( + {'ERROR_INVALID_INPUT'}, + "Provide text editor argument format in File Paths/Applications Preferences, " + "see input field tool-tip for more information.", + ) + return {'CANCELLED'} + + if "$filepath" not in text_editor_args: + self.report({'ERROR_INVALID_INPUT'}, "Text Editor Args Format must contain $filepath") + return {'CANCELLED'} + + args = [text_editor] + template_vars = { + "filepath": self.filepath, + "line": self.line + 1, + "column": self.column + 1, + "line0": self.line, + "column0": self.column, + } + + try: + args.extend([Template(arg).substitute(**template_vars) for arg in shlex.split(text_editor_args)]) + except Exception as ex: + self.report({'ERROR'}, "Exception parsing template: %r" % ex) + return {'CANCELLED'} + + try: + # With `check=True` if `process.returncode != 0` an exception will be raised. + subprocess.run(args, check=True) + except Exception as ex: + self.report({'ERROR'}, "Exception running external editor: %r" % ex) + return {'CANCELLED'} + + return {'FINISHED'} + + +classes = ( + TEXT_OT_jump_to_file_at_point, +) diff --git a/scripts/startup/bl_ui/space_text.py b/scripts/startup/bl_ui/space_text.py index 68a19a23581..43d308c12fc 100644 --- a/scripts/startup/bl_ui/space_text.py +++ b/scripts/startup/bl_ui/space_text.py @@ -256,7 +256,13 @@ class TEXT_MT_text(Menu): if text: layout.separator() - layout.operator("text.reload") + row = layout.row() + row.operator("text.reload") + row.enabled = not text.is_in_memory + + row = layout.row() + op = row.operator("text.jump_to_file_at_point", text="Edit Externally") + row.enabled = (not text.is_in_memory and context.preferences.filepaths.text_editor != "") layout.separator() layout.operator("text.save", icon='FILE_TICK') diff --git a/scripts/startup/bl_ui/space_userpref.py b/scripts/startup/bl_ui/space_userpref.py index 59ade3b31c4..7740188a700 100644 --- a/scripts/startup/bl_ui/space_userpref.py +++ b/scripts/startup/bl_ui/space_userpref.py @@ -10,6 +10,7 @@ from bpy.app.translations import ( pgettext_iface as iface_, pgettext_tip as tip_, ) +from bl_ui.utils import PresetPanel # ----------------------------------------------------------------------------- @@ -1399,6 +1400,13 @@ class USERPREF_PT_file_paths_render(FilePathsPanel, Panel): col.prop(paths, "render_cache_directory", text="Render Cache") +class USERPREF_PT_text_editor_presets(PresetPanel, Panel): + bl_label = "Text Editor Presets" + preset_subdir = "text_editor" + preset_operator = "script.execute_preset" + preset_add_operator = "text_editor.preset_add" + + class USERPREF_PT_file_paths_applications(FilePathsPanel, Panel): bl_label = "Applications" @@ -1416,6 +1424,25 @@ class USERPREF_PT_file_paths_applications(FilePathsPanel, Panel): col.prop(paths, "animation_player", text="Player") +class USERPREF_PT_text_editor(FilePathsPanel, Panel): + bl_label = "Text Editor" + bl_parent_id = "USERPREF_PT_file_paths_applications" + + def draw_header_preset(self, _context): + USERPREF_PT_text_editor_presets.draw_panel_header(self.layout) + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + paths = context.preferences.filepaths + + col = layout.column() + col.prop(paths, "text_editor", text="Program") + col.prop(paths, "text_editor_args", text="Arguments") + + class USERPREF_PT_file_paths_development(FilePathsPanel, Panel): bl_label = "Development" @@ -2510,6 +2537,8 @@ classes = ( USERPREF_PT_file_paths_script_directories, USERPREF_PT_file_paths_render, USERPREF_PT_file_paths_applications, + USERPREF_PT_text_editor, + USERPREF_PT_text_editor_presets, USERPREF_PT_file_paths_development, USERPREF_PT_file_paths_asset_libraries, diff --git a/source/blender/editors/interface/interface_ops.cc b/source/blender/editors/interface/interface_ops.cc index 39b97823043..4f268c5284f 100644 --- a/source/blender/editors/interface/interface_ops.cc +++ b/source/blender/editors/interface/interface_ops.cc @@ -1746,9 +1746,22 @@ static int editsource_text_edit(bContext *C, Main *bmain = CTX_data_main(C); Text *text = nullptr; - /* Developers may wish to copy-paste to an external editor. */ - printf("%s:%d\n", filepath, line); + if (U.text_editor[0] != '\0') { + wmOperatorType *ot = WM_operatortype_find("TEXT_OT_jump_to_file_at_point", true); + PointerRNA op_props; + WM_operator_properties_create_ptr(&op_props, ot); + RNA_string_set(&op_props, "filepath", filepath); + RNA_int_set(&op_props, "line", line - 1); + RNA_int_set(&op_props, "column", 0); + + int result = WM_operator_name_call_ptr(C, ot, WM_OP_EXEC_DEFAULT, &op_props, NULL); + WM_operator_properties_free(&op_props); + + if (result & OPERATOR_FINISHED) { + return OPERATOR_FINISHED; + } + } LISTBASE_FOREACH (Text *, text_iter, &bmain->texts) { if (text_iter->filepath && BLI_path_cmp(text_iter->filepath, filepath) == 0) { text = text_iter; diff --git a/source/blender/makesdna/DNA_userdef_types.h b/source/blender/makesdna/DNA_userdef_types.h index 98a11b2b461..68b88e2a47e 100644 --- a/source/blender/makesdna/DNA_userdef_types.h +++ b/source/blender/makesdna/DNA_userdef_types.h @@ -729,6 +729,9 @@ typedef struct UserDef { /** 1024 = FILE_MAX. */ char image_editor[1024]; /** 1024 = FILE_MAX. */ + char text_editor[1024]; + char text_editor_args[256]; + /** 1024 = FILE_MAX. */ char anim_player[1024]; int anim_player_preset; diff --git a/source/blender/makesrna/intern/rna_userdef.c b/source/blender/makesrna/intern/rna_userdef.c index ab218c1d83f..51382795fe4 100644 --- a/source/blender/makesrna/intern/rna_userdef.c +++ b/source/blender/makesrna/intern/rna_userdef.c @@ -6507,6 +6507,29 @@ static void rna_def_userdef_filepaths(BlenderRNA *brna) RNA_def_property_string_sdna(prop, NULL, "image_editor"); RNA_def_property_ui_text(prop, "Image Editor", "Path to an image editor"); + prop = RNA_def_property(srna, "text_editor", PROP_STRING, PROP_FILEPATH); + RNA_def_property_string_sdna(prop, NULL, "text_editor"); + RNA_def_property_ui_text(prop, + "Text Editor", + "Command to launch the text editor, " + "either a full path or a command in $PATH.\n" + "Use the internal editor when left blank"); + + prop = RNA_def_property(srna, "text_editor_args", PROP_STRING, PROP_NONE); + RNA_def_property_string_sdna(prop, NULL, "text_editor_args"); + RNA_def_property_ui_text( + prop, + "Text Editor Args", + "Defines the specific format of the arguments with which the text editor opens files. " + "The supported expansions are as follows:\n" + "\n" + "$filepath The absolute path of the file.\n" + "$line The line to open at (Optional).\n" + "$column The column to open from the beginning of the line (Optional).\n" + "$line0 & column0 start at zero." + "\n" + "Example: -f $filepath -l $line -c $column"); + prop = RNA_def_property(srna, "animation_player", PROP_STRING, PROP_FILEPATH); RNA_def_property_string_sdna(prop, NULL, "anim_player"); RNA_def_property_ui_text(