diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 197fae7184..b197639840 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -607,6 +607,7 @@ src/main/help/help/topics/Trees/images/FilterOptions.png||GHIDRA||||END| src/main/help/help/topics/Trees/images/TableColumnFilter.png||GHIDRA||||END| src/main/help/help/topics/Trees/images/TableColumnFilterAfterFilterApplied.png||GHIDRA||||END| src/main/help/help/topics/Trees/images/TableColumnFilterDialog.png||GHIDRA||||END| +src/main/help/help/topics/VSCodeIntegration/VSCodeIntegration.htm||GHIDRA||||END| src/main/help/help/topics/ValidateProgram/ValidateProgram.html||GHIDRA||||END| src/main/help/help/topics/ValidateProgram/images/ValidateProgram.png||GHIDRA||||END| src/main/help/help/topics/ValidateProgram/images/ValidateProgramDone.png||GHIDRA||||END| @@ -934,6 +935,7 @@ src/main/resources/images/view_left_right.png||Nuvola Icons - LGPL 2.1|||Nuvola src/main/resources/images/view_top_bottom.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| src/main/resources/images/viewmag+.png||Crystal Clear Icons - LGPL 2.1||||END| src/main/resources/images/viewmag.png||GHIDRA||||END| +src/main/resources/images/vscode.png||MIT||||END| src/main/resources/images/window.png||GHIDRA||||END| src/main/resources/images/wizard.png||Nuvola Icons - LGPL 2.1|||nuvola|END| src/main/resources/images/x-office-document-template.png||Tango Icons - Public Domain|||tango icon set|END| diff --git a/Ghidra/Features/Base/data/base.icons.theme.properties b/Ghidra/Features/Base/data/base.icons.theme.properties index 05628777bb..cad3f267d1 100644 --- a/Ghidra/Features/Base/data/base.icons.theme.properties +++ b/Ghidra/Features/Base/data/base.icons.theme.properties @@ -267,6 +267,7 @@ icon.plugin.scriptmanager.run = icon.run icon.plugin.scriptmanager.run.again = play_again.png icon.plugin.scriptmanager.edit = accessories-text-editor.png icon.plugin.scriptmanager.edit.eclipse = eclipse.png +icon.plugin.scriptmanager.edit.vscode = vscode.png icon.plugin.scriptmanager.keybinding = key.png icon.plugin.scriptmanager.delete = icon.delete icon.plugin.scriptmanager.rename = icon.rename diff --git a/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm b/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm index 884ee56e12..0a759195eb 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm @@ -210,6 +210,17 @@ Ghidra scripts in Eclipse, see Extensions/Eclipse/GhidraDev/GhidraDev_README.html.
+ +
+Edits the selected script in Visual Studio Code. +
+++
Before a script can be edited in + Visual Studio Code, a Visual Studio Code executable path must be defined in the Tool's + Visual Studio Code Integration + options if Visual Studio Code is installed in a non-default location.

Ghidra is capable of integrating with an existing Visual Studio Code installation to aid + in the development of Ghidra scripts and modules.
+ +
The following Front-End tool options (Edit -> Tool Options) may need to be configured for + Ghidra to successfully launch Visual Studio Code on your platform.
+|
+ Tool Options + |
+ |
| Option | +Description | +
| Visual Studio Code Executable Path | +Path to a Visual Studio Code executable file. It defaults + to the most commonly used location on your specific platform. | +
This action creates a new Visual Studio Code project folder which can be used as a convenient + starting point to develop a new Ghidra module. The new project will be linked against the + version of Ghidra that was used to create the project. Once the project is created, the + associated Ghidra version cannot change. This behavior may become more flexible in the + future.
+ +Visual Studio Code launchers are provided which will allow you to debug your module's code. + Also, a Gradle task named ghidra/distributeExtension is provided that will allow you to + build a distributable Ghidra extension. +
+ + + diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/eclipse/EclipseIntegrationOptionsPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/eclipse/EclipseIntegrationOptionsPlugin.java index 811d3545ae..a89678bd35 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/eclipse/EclipseIntegrationOptionsPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/eclipse/EclipseIntegrationOptionsPlugin.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -35,7 +35,7 @@ import ghidra.util.HelpLocation; packageName = CorePluginPackage.NAME, category = PluginCategoryNames.COMMON, shortDescription = "Eclipse Integration Options", - description = "Options Eclipse Integration" + description = "Options for Eclipse Integration" ) //@formatter:on public class EclipseIntegrationOptionsPlugin extends Plugin implements ApplicationLevelOnlyPlugin { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java index d7170bf635..d7eff4e9ac 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -226,6 +226,9 @@ class GhidraScriptActionManager { createScriptAction("EditEclipse", "Edit with Eclipse", "Edit Script with Eclipse", new GIcon("icon.plugin.scriptmanager.edit.eclipse"), null, provider::editScriptEclipse); + createScriptAction("EditVSCode", "Edit with VSCode", "Edit Script with Visual Studio Code", + new GIcon("icon.plugin.scriptmanager.edit.vscode"), null, provider::editScriptVSCode); + keyBindingAction = createScriptAction("Key Binding", "Assign Key Binding", "Assign Key Binding", new GIcon("icon.plugin.scriptmanager.keybinding"), null, diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java index 9e98940341..b11b61e6a6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java @@ -927,6 +927,20 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter { plugin.tryToEditFileInEclipse(script); } + void editScriptVSCode() { + ResourceFile script = getSelectedScript(); + if (script == null) { + plugin.getTool().setStatusInfo("Script is null."); + return; + } + if (!script.exists()) { + plugin.getTool().setStatusInfo("Script " + script.getName() + " does not exist."); + return; + } + + plugin.tryToEditFileInVSCode(script); + } + GhidraScriptEditorComponentProvider editScriptInGhidra(ResourceFile script) { GhidraScriptEditorComponentProvider editor = editorMap.get(script); if (editor == null) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java index 1aba81aaae..cc3eb2302c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -155,6 +155,13 @@ public class GhidraScriptMgrPlugin extends ProgramPlugin implements GhidraScript } } + @Override + public boolean tryToEditFileInVSCode(ResourceFile file) { + VSCodeIntegrationService service = tool.getService(VSCodeIntegrationService.class); + service.launchVSCode(file.getFile(false)); + return true; + } + @Override protected void programClosed(Program program) { provider.programClosed(program); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationOptionsPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationOptionsPlugin.java new file mode 100644 index 0000000000..749be4ba4d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationOptionsPlugin.java @@ -0,0 +1,77 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.vscode; + +import java.io.File; + +import ghidra.app.CorePluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.OperatingSystem; +import ghidra.framework.main.ApplicationLevelOnlyPlugin; +import ghidra.framework.options.OptionType; +import ghidra.framework.options.ToolOptions; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.util.HelpLocation; + +/** + * {@link Plugin} responsible for registering Visual Studio Code-related options + */ +//@formatter:off +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = CorePluginPackage.NAME, + category = PluginCategoryNames.COMMON, + shortDescription = "Visual Studio Code Integration Options", + description = "Options for Visual Studio Code Integration" +) +//@formatter:on +public class VSCodeIntegrationOptionsPlugin extends Plugin implements ApplicationLevelOnlyPlugin { + + public static final String PLUGIN_OPTIONS_NAME = "Visual Studio Code Integration"; + + public static final String VSCODE_EXE_PATH_OPTION = "Visual Studio Code Executable Path"; + private static final String VSCODE_EXE_PATH_DESC = "Path to Visual Studio Code executable"; + private static final File VSCODE_EXE_PATH_DEFAULT = getDefaultVSCodeExecutable(); + + public VSCodeIntegrationOptionsPlugin(PluginTool tool) { + super(tool); + } + + @Override + public void init() { + super.init(); + ToolOptions options = tool.getOptions(PLUGIN_OPTIONS_NAME); + options.registerOption(VSCODE_EXE_PATH_OPTION, OptionType.FILE_TYPE, + VSCODE_EXE_PATH_DEFAULT, null, VSCODE_EXE_PATH_DESC); + options.setOptionsHelpLocation( + new HelpLocation("VSCodeIntegration", "VSCodeIntegrationOptions")); + } + + /** + * {@return the default Visual Studio Code executable location for the current platform} + */ + private static File getDefaultVSCodeExecutable() { + return switch (OperatingSystem.CURRENT_OPERATING_SYSTEM) { + case WINDOWS -> new File(System.getenv("LOCALAPPDATA"), + "Programs/Microsoft VS Code/bin/code.cmd"); + case MAC_OS_X -> new File( + "/Applications/Visual Studio Code.app/Contents/MacOS/Electron"); + case LINUX -> new File("/usr/bin/code"); + default -> null; + }; + } +} diff --git a/Ghidra/Features/Base/ghidra_scripts/VSCodeProjectScript.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationPlugin.java similarity index 53% rename from Ghidra/Features/Base/ghidra_scripts/VSCodeProjectScript.java rename to Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationPlugin.java index e805374144..d322f664bb 100644 --- a/Ghidra/Features/Base/ghidra_scripts/VSCodeProjectScript.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationPlugin.java @@ -13,11 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// Creates a new VSCode project for Ghidra script and module development. -// @category Development +package ghidra.app.plugin.core.vscode; -import java.io.File; -import java.io.IOException; +import java.io.*; import java.nio.charset.StandardCharsets; import java.util.*; @@ -25,22 +23,81 @@ import org.apache.commons.io.*; import com.google.gson.*; +import docking.DockingWindowManager; +import docking.action.builder.ActionBuilder; +import docking.options.OptionsService; +import docking.tool.ToolConstants; +import docking.widgets.OptionDialog; +import docking.widgets.values.ValuesMapDialog; +import generic.theme.GIcon; import ghidra.*; -import ghidra.app.script.GhidraScript; +import ghidra.app.CorePluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.ProgramPlugin; +import ghidra.app.services.VSCodeIntegrationService; import ghidra.features.base.values.GhidraValuesMap; import ghidra.framework.Application; import ghidra.framework.ApplicationProperties; -import ghidra.util.SystemUtilities; +import ghidra.framework.main.AppInfo; +import ghidra.framework.options.ToolOptions; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.util.*; +import ghidra.util.task.TaskLauncher; import utilities.util.FileUtilities; -public class VSCodeProjectScript extends GhidraScript { +/** + * {@link Plugin} responsible integrating Ghidra with Visual Studio Code + */ +//@formatter:off +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = CorePluginPackage.NAME, + category = PluginCategoryNames.COMMON, + shortDescription = "Visual Studio Code Integration", + description = "Allows Ghidra to integrate with Visual Studio Code.", + servicesRequired = { OptionsService.class }, + servicesProvided = { VSCodeIntegrationService.class } +) +//@formatter:on +public class VSCodeIntegrationPlugin extends ProgramPlugin implements VSCodeIntegrationService { + private ToolOptions options; private Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + /** + * Create a new {@link VSCodeIntegrationPlugin} + * + * @param tool The associated {@link PluginTool tool} + */ + public VSCodeIntegrationPlugin(PluginTool tool) { + super(tool); + } + @Override - protected void run() throws Exception { + public void init() { + super.init(); + + options = AppInfo.getFrontEndTool().getOptions( + VSCodeIntegrationOptionsPlugin.PLUGIN_OPTIONS_NAME); + + new ActionBuilder("CreateVSCodeModuleProject", name) + .menuPath(ToolConstants.MENU_TOOLS, "Create VSCode Module Project...") + .menuIcon(new GIcon("icon.plugin.scriptmanager.edit.vscode")) + .description("Creates a new Visual Studio Code module project.") + .helpLocation(new HelpLocation("VSCodeIntegration", "VSCodeModuleProject")) + .onAction(context -> showNewProjectDialog()) + .buildAndInstall(tool); + } + + /** + * Present the user with a dialog that allows them to select a name and location for their new + * Visual Studio Code Module Project + */ + private void showNewProjectDialog() { if (!SystemUtilities.isInReleaseMode()) { - printerr("This script may only run from a built Ghidra release."); + Msg.showInfo(this, tool.getToolFrame(), name, + "This action may only run from a built Ghidra release."); return; } @@ -50,27 +107,132 @@ public class VSCodeProjectScript extends GhidraScript { GhidraValuesMap values = new GhidraValuesMap(); values.defineString(PROJECT_NAME_PROMPT); values.defineDirectory(PROJECT_ROOT_PROMPT, new File(System.getProperty("user.home"))); - values = askValues("Setup New VSCode Project", null, values); + ValuesMapDialog dialog = + new ValuesMapDialog("Create New Visual Studio Code Module Project", null, values); + DockingWindowManager.showDialog(dialog); + if (dialog.isCancelled()) { + return; + } + values = (GhidraValuesMap) dialog.getValues(); String projectName = values.getString(PROJECT_NAME_PROMPT); File projectRootDir = values.getFile(PROJECT_ROOT_PROMPT); File projectDir = new File(projectRootDir, projectName); if (projectDir.exists()) { - printerr("Directory '%s' already exists...exiting".formatted(projectDir)); + Msg.showError(this, tool.getToolFrame(), name, + "Directory '%s' already exists...exiting".formatted(projectDir)); return; } + try { + createVSCodeModuleProject(projectDir); + Msg.showInfo(this, tool.getToolFrame(), name, + "Successfully created Visual Studio Code module project directory at: " + + projectDir); + } + catch (IOException e) { + Msg.showError(this, tool.getToolFrame(), name, + "Failed to create Visual Studio Code module project directory at: " + projectDir, + e); + } + } + + @Override + public ToolOptions getVSCodeIntegrationOptions() { + return options; + } + + @Override + public File getVSCodeExecutableFile() throws FileNotFoundException { + File vscodeExecutableFile = + options.getFile(VSCodeIntegrationOptionsPlugin.VSCODE_EXE_PATH_OPTION, null); + if (vscodeExecutableFile == null || !vscodeExecutableFile.isFile()) { + throw new FileNotFoundException( + "Visual Studio Code installation executable file does not exist."); + } + return vscodeExecutableFile; + } + + @Override + public void launchVSCode(File file) { + TaskLauncher.launch(new VSCodeLauncherTask(this, file)); + } + + @Override + public void handleVSCodeError(String error, boolean askAboutOptions, Throwable t) { + if (askAboutOptions && !SystemUtilities.isInHeadlessMode()) { + SystemUtilities.runSwingNow(() -> { + int choice = + OptionDialog.showYesNoDialog(null, "Failed to launch Visual Studio Code", + error + "\nWould you like to verify your \"" + + VSCodeIntegrationOptionsPlugin.PLUGIN_OPTIONS_NAME + + "\" options now?"); + if (choice == OptionDialog.YES_OPTION) { + AppInfo.getFrontEndTool() + .getService(OptionsService.class) + .showOptionsDialog( + VSCodeIntegrationOptionsPlugin.PLUGIN_OPTIONS_NAME, null); + } + }); + } + else { + Msg.showError(VSCodeIntegrationPlugin.class, null, + "Failed to launch Visual Studio Code", error, t); + } + } + + @Override + public void createVSCodeModuleProject(File projectDir) throws IOException { + File installDir = Application.getInstallationDirectory().getFile(false); Map