diff --git a/Ghidra/Features/Base/src/main/help/help/topics/SourceFilesTablePlugin/SourceFilesTable.html b/Ghidra/Features/Base/src/main/help/help/topics/SourceFilesTablePlugin/SourceFilesTable.html
index 375a464a2a..8181c6f2b6 100644
--- a/Ghidra/Features/Base/src/main/help/help/topics/SourceFilesTablePlugin/SourceFilesTable.html
+++ b/Ghidra/Features/Base/src/main/help/help/topics/SourceFilesTablePlugin/SourceFilesTable.html
@@ -26,8 +26,8 @@
- - A path, which must be an absolute, normalized path using forward slashes (think file
- URI).
+ - A path, which must be an absolute, normalized path using forward slashes.
+ E.g., "/usr/src/main/file.c", "/C:/Users/Ghidra/sourceFile.cc".
- A SourceFileIdType, which can be NONE, UNKNOWN, TIMESTAMP_64, MD5, SHA1, SHA256,
or SHA512.
@@ -81,8 +81,9 @@
Note that adding source files,
- removing source files, or changing the source map requires exclusive access to a program.
- Reading the source file list or source map does not require exclusive access.
+ removing source files, or changing the source map requires an exclusive checkout if
+ the program is in a shared Ghidra repository. Reading the source file list or source map does
+ not require an exclusive checkout.
@@ -111,7 +112,8 @@
path.
- Directory Transforms, which replace a parent directory of a source file's path
- with another directory.
+ with another directory. For example, the directory transform "/src/ -> "/usr/test/files/"
+ would transform the path "/src/dir1/file.c" to "/usr/test/files/dir1/file.c".
@@ -176,15 +178,54 @@
This action removes the selected transform from the list of transforms.
Edit Transform
-
This action allows you to change the destination of a transform (but not the source).
+
+
+ Listing Actions
+
+
+ View Source
+
+ This Listing action is enabled if there is source map information
+ for an address. It will open the corresponding source file at the
+ appropriate line in a source code viewer (currently Eclipse and VS Code
+ are supported). Options for configuring the viewer are described
+ below. If there are
+ multiple source map entries defined for an address, the user will be
+ prompted to select which one to send to the viewer. Performing
+ this action on a particular line of the Source Map Listing field will
+ open the corresponding file in the viewer even if there are multiple
+ entries defined at the current address.
+
+
+
+
+
+ Plugin Options
+
+
+ Use Existing As Default
+
+ If enabled, the SourcePathTransformer will just return a SourceFile's path if
+ no transform applies to the file.
+
+ Viewer for Source Files
+ Selects the viewer to use for source files. The supported viewers are Eclipse
+ and Visual Studio Code. Your viewer of choice must be configured via the
+ appropriate option in the Front-End tool.
+
+
Related Topics:
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTableModel.java
index 7c83bb7c20..3d50aa90a9 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTableModel.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTableModel.java
@@ -22,7 +22,8 @@ import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.database.sourcemap.*;
import ghidra.program.model.listing.Program;
-import ghidra.program.model.sourcemap.*;
+import ghidra.program.model.sourcemap.SourceFileManager;
+import ghidra.program.model.sourcemap.SourcePathTransformer;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@@ -150,6 +151,12 @@ public class SourceFilesTableModel extends ThreadedTableModelStub isEnabled(c))
+ .onAction(c -> viewSourceFile(c))
+ .buildAndInstall(tool);
+ }
+
+ private boolean isEnabled(ListingActionContext context) {
+ Address address = context.getAddress();
+ SourceFileManager manager = getCurrentProgram().getSourceFileManager();
+ return manager.getSourceMapEntries(address).size() > 0;
+ }
+
+ private void viewSourceFile(ListingActionContext context) {
+ Address address = context.getAddress();
+ SourceFileManager manager = getCurrentProgram().getSourceFileManager();
+ List entries = manager.getSourceMapEntries(address);
+ if (entries.isEmpty()) {
+ return; // sanity check
+ }
+ // if there's only one entry associated with the address, just view it
+ if (entries.size() == 1) {
+ openInViewer(entries.get(0));
+ return;
+ }
+ // if there are multiple entries, we need to decide which one to view
+ // if the user right-clicked in the SourceMapField in the Listing, open
+ // the associated entry
+ if (context.getLocation() instanceof SourceMapFieldLocation sourceLoc) {
+ openInViewer(sourceLoc.getSourceMapEntry());
+ return;
+ }
+ // otherwise pop up a window and ask the user to select an entry
+ GValuesMap valuesMap = new GValuesMap();
+ Map stringsToEntries = new HashMap<>();
+ for (SourceMapEntry entry : entries) {
+ stringsToEntries.put(entry.toString(), entry);
+ }
+ valuesMap.defineChoice("Entry", entries.get(0).toString(),
+ stringsToEntries.keySet().toArray(new String[0]));
+ ValuesMapDialog dialog =
+ new ValuesMapDialog("Select Entry to View", null, valuesMap);
+ DockingWindowManager.showDialog(dialog);
+ if (dialog.isCancelled()) {
+ return;
+ }
+ GValuesMap results = dialog.getValues();
+ if (results == null) {
+ return;
+ }
+ String selected = results.getChoice("Entry");
+ if (selected == null) {
+ return;
+ }
+ SourceMapEntry entryToShow = stringsToEntries.get(selected);
+ openInViewer(entryToShow);
+ }
+
+ private void openInViewer(SourceMapEntry entry) {
+ if (entry == null) {
+ return;
+ }
+ SourcePathTransformer transformer =
+ UserDataPathTransformer.getPathTransformer(currentProgram);
+ String transformedPath =
+ transformer.getTransformedPath(entry.getSourceFile(), useExistingAsDefault);
+
+ if (transformedPath == null) {
+ Msg.showWarn(this, null, "No Path Transform",
+ "No path transformation applies to " + entry.getSourceFile().toString());
+ return;
+ }
+
+ File localSourceFile = new File(transformedPath);
+ if (!localSourceFile.exists()) {
+ Msg.showWarn(transformer, null, "File Not Found",
+ localSourceFile.getAbsolutePath() + " does not exist");
+ return;
+ }
+
+ switch (selectedViewer) {
+ case ECLIPSE:
+ openFileInEclipse(localSourceFile.getAbsolutePath(), entry.getLineNumber());
+ break;
+ case VS_CODE:
+ openFileInVsCode(localSourceFile.getAbsolutePath(), entry.getLineNumber());
+ break;
+ default:
+ throw new AssertionError("Unsupported Viewer: " + selectedViewer);
+ }
+ }
+
+ private void openFileInEclipse(String path, int lineNumber) {
+ EclipseIntegrationService eclipseService = tool.getService(EclipseIntegrationService.class);
+ if (eclipseService == null) {
+ Msg.showError(this, null, "Eclipse Service Error",
+ "Eclipse service not configured for tool");
+ return;
+ }
+
+ try {
+ eclipseExecutable = eclipseService.getEclipseExecutableFile();
+ }
+ catch (FileNotFoundException e) {
+ Msg.showError(this, null, "Missing Eclipse Executable", e.getMessage());
+ return;
+ }
+ MonitoredRunnable r = m -> {
+ try {
+ List args = new ArrayList<>();
+ args.add(eclipseExecutable.getAbsolutePath());
+ args.add(path + ":" + lineNumber);
+ new ProcessBuilder(args).redirectErrorStream(true).start();
+ }
+ catch (Exception e) {
+ eclipseService.handleEclipseError(
+ "Unexpected exception occurred while launching Eclipse.", false,
+ null);
+ return;
+ }
+ };
+
+ new TaskBuilder("Opening File in Eclipse", r)
+ .setHasProgress(false)
+ .setCanCancel(true)
+ .launchModal();
+ return;
+
+ }
+
+ private void openFileInVsCode(String path, int lineNumber) {
+ VSCodeIntegrationService vscodeService = tool.getService(VSCodeIntegrationService.class);
+ if (vscodeService == null) {
+ Msg.showError(this, null, "VSCode Service Error",
+ "VSCode service not configured for tool");
+ return;
+ }
+
+ try {
+ vscodeExecutable = vscodeService.getVSCodeExecutableFile();
+ }
+ catch (FileNotFoundException e) {
+ Msg.showError(this, null, "Missing VSCode executable", e.getMessage());
+ return;
+ }
+
+ MonitoredRunnable r = m -> {
+ try {
+ List args = new ArrayList<>();
+ args.add(vscodeExecutable.getAbsolutePath());
+ args.add("--goto");
+ args.add(path + ":" + lineNumber);
+ new ProcessBuilder(args).redirectErrorStream(true).start();
+ }
+ catch (Exception e) {
+ vscodeService.handleVSCodeError(
+ "Unexpected exception occurred while launching Visual Studio Code.", false,
+ null);
+ return;
+ }
+ };
+
+ new TaskBuilder("Opening File in VSCode", r)
+ .setHasProgress(false)
+ .setCanCancel(true)
+ .launchModal();
+ return;
+
+ }
+
+
+ private void initOptions(ToolOptions options) {
+ options.registerOption(USE_EXISTING_AS_DEFAULT_OPTION_NAME, true,
+ new HelpLocation(getName(), "Use_Existing_As_Default"),
+ "Use a source file's existing path if no transform applies");
+ useExistingAsDefault = options.getBoolean(USE_EXISTING_AS_DEFAULT_OPTION_NAME, true);
+
+ options.registerOption(SELECTED_VIEWER_OPTION_NAME,
+ OptionType.STRING_TYPE, VS_CODE,
+ new HelpLocation(getName(), "Viewer_for_Source_Files"),
+ "Viewer for Source Files",
+ () -> new StringWithChoicesEditor(VIEWERS));
+ selectedViewer = options.getString(SELECTED_VIEWER_OPTION_NAME, VS_CODE);
+ options.addOptionsChangeListener(this);
+ }
+
+
+
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/format/FormatManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/format/FormatManager.java
index 4e3e47e354..90242c486e 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/format/FormatManager.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/format/FormatManager.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.
@@ -591,6 +591,22 @@ public class FormatManager implements OptionsChangeListener {
rowElem.addContent(colElem);
root.addContent(rowElem);
+
+ rowElem = new Element("ROW");
+
+ colElem = new Element("FIELD");
+ colElem.setAttribute("WIDTH", "200");
+ colElem.setAttribute("ENABLED", "true");
+ rowElem.addContent(colElem);
+
+ colElem = new Element("FIELD");
+ colElem.setAttribute("NAME", "Source Map");
+ colElem.setAttribute("WIDTH", "440");
+ colElem.setAttribute("ENABLED", "true");
+ rowElem.addContent(colElem);
+
+ root.addContent(rowElem);
+
rowElem = new Element("ROW");
colElem = new Element("FIELD");