diff --git a/CHANGELOG.md b/CHANGELOG.md index 909a75108b0..85fa26fb451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added a switch not to store the linked file URL, because it caused troubles at other apps. [#11735](https://github.com/JabRef/jabref/pull/11735) - When starting a new SLR, the selected catalogs now persist within and across JabRef sessions. [koppor#614](https://github.com/koppor/jabref/issues/614) - We added support for drag'n'drop on an entry in the maintable to an external application to get the entry preview dropped. [#11846](https://github.com/JabRef/jabref/pull/11846) +- We added the functionality to double click on a [LaTeX citation](https://docs.jabref.org/advanced/entryeditor/latex-citations) to jump to the respective line in the LaTeX editor. [#11996](https://github.com/JabRef/jabref/issues/11996) - We added a different background color to the search bar to indicate when the search syntax is wrong. [#11658](https://github.com/JabRef/jabref/pull/11658) - We added a setting which always adds the literal "Cited on pages" text before each JStyle citation. [#11691](https://github.com/JabRef/jabref/pull/11732) - We added a new plain citation parser that uses LLMs. [#11825](https://github.com/JabRef/jabref/issues/11825) diff --git a/src/main/java/org/jabref/gui/entryeditor/LatexCitationsTab.java b/src/main/java/org/jabref/gui/entryeditor/LatexCitationsTab.java index 6e45f1d491e..7114b863bba 100644 --- a/src/main/java/org/jabref/gui/entryeditor/LatexCitationsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/LatexCitationsTab.java @@ -56,8 +56,10 @@ public LatexCitationsTab(BibDatabaseContext databaseContext, private void setSearchPane() { progressIndicator.setMaxSize(100, 100); + citationsDisplay.basePathProperty().bindBidirectional(viewModel.directoryProperty()); citationsDisplay.setItems(viewModel.getCitationList()); + citationsDisplay.setOnMouseClicked(event -> viewModel.handleMouseClick(event, citationsDisplay)); RowConstraints mainRow = new RowConstraints(); mainRow.setVgrow(Priority.ALWAYS); diff --git a/src/main/java/org/jabref/gui/entryeditor/LatexCitationsTabViewModel.java b/src/main/java/org/jabref/gui/entryeditor/LatexCitationsTabViewModel.java index b4fa51069b3..dd4b46bbec0 100644 --- a/src/main/java/org/jabref/gui/entryeditor/LatexCitationsTabViewModel.java +++ b/src/main/java/org/jabref/gui/entryeditor/LatexCitationsTabViewModel.java @@ -15,10 +15,16 @@ import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; import org.jabref.gui.AbstractViewModel; import org.jabref.gui.DialogService; import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.push.PushToApplication; +import org.jabref.gui.push.PushToApplications; +import org.jabref.gui.push.PushToTeXstudio; +import org.jabref.gui.texparser.CitationsDisplay; import org.jabref.gui.util.DirectoryDialogConfiguration; import org.jabref.gui.util.UiTaskExecutor; import org.jabref.logic.l10n.Localization; @@ -154,6 +160,22 @@ public void onDirectoryDelete(File directory) { }; } + public void handleMouseClick(MouseEvent event, CitationsDisplay citationsDisplay) { + Citation selectedItem = citationsDisplay.getSelectionModel().getSelectedItem(); + + if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2 && selectedItem != null) { + String applicationName = preferences.getPushToApplicationPreferences() + .getActiveApplicationName(); + PushToApplication application = PushToApplications.getApplicationByName( + applicationName, + dialogService, + preferences) + .orElse(new PushToTeXstudio(dialogService, preferences)); + preferences.getPushToApplicationPreferences().setActiveApplicationName(application.getDisplayName()); + application.jumpToLine(selectedItem.path(), selectedItem.line(), selectedItem.colStart()); + } + } + public void bindToEntry(BibEntry entry) { checkAndUpdateDirectory(); diff --git a/src/main/java/org/jabref/gui/push/AbstractPushToApplication.java b/src/main/java/org/jabref/gui/push/AbstractPushToApplication.java index 40c04c97a8d..15615c88691 100644 --- a/src/main/java/org/jabref/gui/push/AbstractPushToApplication.java +++ b/src/main/java/org/jabref/gui/push/AbstractPushToApplication.java @@ -1,6 +1,7 @@ package org.jabref.gui.push; import java.io.IOException; +import java.nio.file.Path; import java.util.List; import java.util.Optional; @@ -181,4 +182,28 @@ public Optional getKeyBinding() { return Optional.of(KeyBinding.PUSH_TO_APPLICATION); } } + + public void jumpToLine(Path fileName, int line, int column) { + commandPath = preferences.getPushToApplicationPreferences().getCommandPaths().get(this.getDisplayName()); + + if (StringUtil.isNullOrEmpty(commandPath)) { + notDefined = true; + return; + } + + String[] command = jumpToLineCommandlineArguments(fileName, line, column); + ProcessBuilder processBuilder = new ProcessBuilder(); + try { + processBuilder.command(command); + processBuilder.start(); + } catch (IOException excep) { + LOGGER.warn("Error: Could not call executable '{}'", commandPath, excep); + couldNotCall = true; + } + } + + protected String[] jumpToLineCommandlineArguments(Path fileName, int line, int column) { + LOGGER.error("Not yet implemented"); + return new String[0]; + } } diff --git a/src/main/java/org/jabref/gui/push/PushToApplication.java b/src/main/java/org/jabref/gui/push/PushToApplication.java index 18e012e904f..9292b433fb9 100644 --- a/src/main/java/org/jabref/gui/push/PushToApplication.java +++ b/src/main/java/org/jabref/gui/push/PushToApplication.java @@ -1,5 +1,6 @@ package org.jabref.gui.push; +import java.nio.file.Path; import java.util.List; import org.jabref.gui.actions.Action; @@ -56,4 +57,6 @@ public interface PushToApplication { PushToApplicationSettings getSettings(PushToApplication application, PushToApplicationPreferences pushToApplicationPreferences); String getDelimiter(); + + void jumpToLine(Path fileName, int line, int column); } diff --git a/src/main/java/org/jabref/gui/push/PushToApplicationCommand.java b/src/main/java/org/jabref/gui/push/PushToApplicationCommand.java index c5fa4c905eb..5ab1c532d4d 100644 --- a/src/main/java/org/jabref/gui/push/PushToApplicationCommand.java +++ b/src/main/java/org/jabref/gui/push/PushToApplicationCommand.java @@ -104,7 +104,7 @@ private static String getKeyString(List entries, String delimiter) { for (BibEntry bes : entries) { citeKey = bes.getCitationKey(); if (citeKey.isEmpty() || citeKey.get().isEmpty()) { - // Should never occur, because we made sure that all entries have keys + LOGGER.warn("Should never occur, because we made sure that all entries have keys"); continue; } if (first) { diff --git a/src/main/java/org/jabref/gui/push/PushToEmacs.java b/src/main/java/org/jabref/gui/push/PushToEmacs.java index c3d612b2d48..0139dc02da1 100644 --- a/src/main/java/org/jabref/gui/push/PushToEmacs.java +++ b/src/main/java/org/jabref/gui/push/PushToEmacs.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; @@ -147,4 +148,9 @@ protected String getCommandName() { public PushToApplicationSettings getSettings(PushToApplication application, PushToApplicationPreferences preferences) { return new PushToEmacsSettings(application, dialogService, this.preferences.getFilePreferences(), preferences); } + + @Override + protected String[] jumpToLineCommandlineArguments(Path fileName, int line, int column) { + return new String[] {commandPath, "+%s".formatted(line), fileName.toString()}; + } } diff --git a/src/main/java/org/jabref/gui/push/PushToSublimeText.java b/src/main/java/org/jabref/gui/push/PushToSublimeText.java index af0820e7ab2..fbcef0b4cc2 100644 --- a/src/main/java/org/jabref/gui/push/PushToSublimeText.java +++ b/src/main/java/org/jabref/gui/push/PushToSublimeText.java @@ -86,4 +86,9 @@ protected String[] getCommandLine(String keyString) { return new String[] {"sh", "-c", "\"" + commandPath + "\"" + " --command 'insert {\"characters\": \"" + citeCommand + keyString + getCiteSuffix() + "\"}'"}; } } + + @Override + protected String[] jumpToLineCommandlineArguments(Path fileName, int line, int column) { + return new String[] {commandPath, "%s:%s:%s".formatted(fileName.toString(), line, column)}; + } } diff --git a/src/main/java/org/jabref/gui/push/PushToTeXstudio.java b/src/main/java/org/jabref/gui/push/PushToTeXstudio.java index 17c6d5650c6..ac5ba327275 100644 --- a/src/main/java/org/jabref/gui/push/PushToTeXstudio.java +++ b/src/main/java/org/jabref/gui/push/PushToTeXstudio.java @@ -1,5 +1,7 @@ package org.jabref.gui.push; +import java.nio.file.Path; + import org.jabref.gui.DialogService; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.icon.JabRefIcon; @@ -27,4 +29,9 @@ public JabRefIcon getApplicationIcon() { protected String[] getCommandLine(String keyString) { return new String[] {commandPath, "--insert-cite", "%s%s%s".formatted(getCitePrefix(), keyString, getCiteSuffix())}; } + + @Override + public String[] jumpToLineCommandlineArguments(Path fileName, int line, int column) { + return new String[] {commandPath, "--line", Integer.toString(line), fileName.toString()}; + } } diff --git a/src/main/java/org/jabref/gui/push/PushToTeXworks.java b/src/main/java/org/jabref/gui/push/PushToTeXworks.java index c59df18ba0f..6459d848c13 100644 --- a/src/main/java/org/jabref/gui/push/PushToTeXworks.java +++ b/src/main/java/org/jabref/gui/push/PushToTeXworks.java @@ -1,5 +1,7 @@ package org.jabref.gui.push; +import java.nio.file.Path; + import org.jabref.gui.DialogService; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.icon.JabRefIcon; @@ -33,4 +35,10 @@ public JabRefIcon getApplicationIcon() { protected String[] getCommandLine(String keyString) { return new String[] {commandPath, "--insert-text", "%s%s%s".formatted(getCitePrefix(), keyString, getCiteSuffix())}; } + + @Override + protected String[] jumpToLineCommandlineArguments(Path fileName, int line, int column) { + // No command known to jump to a specific line + return new String[] {commandPath, fileName.toString()}; + } } diff --git a/src/main/java/org/jabref/gui/push/PushToTexmaker.java b/src/main/java/org/jabref/gui/push/PushToTexmaker.java index 75bae32116a..509fb6626ff 100644 --- a/src/main/java/org/jabref/gui/push/PushToTexmaker.java +++ b/src/main/java/org/jabref/gui/push/PushToTexmaker.java @@ -1,5 +1,7 @@ package org.jabref.gui.push; +import java.nio.file.Path; + import org.jabref.gui.DialogService; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.icon.JabRefIcon; @@ -30,4 +32,9 @@ public JabRefIcon getApplicationIcon() { protected String[] getCommandLine(String keyString) { return new String[] {commandPath, "-insert", getCitePrefix() + keyString + getCiteSuffix()}; } + + @Override + protected String[] jumpToLineCommandlineArguments(Path fileName, int line, int column) { + return new String[] {commandPath, "-line", Integer.toString(line), fileName.toString()}; + } } diff --git a/src/main/java/org/jabref/gui/push/PushToVim.java b/src/main/java/org/jabref/gui/push/PushToVim.java index 8702cd11eed..3e2f3d4fb04 100644 --- a/src/main/java/org/jabref/gui/push/PushToVim.java +++ b/src/main/java/org/jabref/gui/push/PushToVim.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; @@ -10,6 +11,7 @@ import org.jabref.gui.icon.JabRefIcon; import org.jabref.gui.preferences.GuiPreferences; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.os.OS; import org.jabref.logic.util.HeadlessExecutorService; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -44,14 +46,7 @@ public PushToApplicationSettings getSettings(PushToApplication application, Push @Override public void pushEntries(BibDatabaseContext database, List entries, String keys) { - couldNotPush = false; - couldNotCall = false; - notDefined = false; - - commandPath = preferences.getPushToApplicationPreferences().getCommandPaths().get(this.getDisplayName()); - - if ((commandPath == null) || commandPath.trim().isEmpty()) { - notDefined = true; + if (!determineCommandPath()) { return; } @@ -105,4 +100,65 @@ public void onOperationCompleted() { super.onOperationCompleted(); } } + + @Override + public void jumpToLine(Path fileName, int line, int column) { + if (!determineCommandPath()) { + return; + } + + ProcessBuilder processBuilder = new ProcessBuilder(); + try { + String[] command = jumpToLineCommandlineArguments(fileName, line, column); + if (OS.WINDOWS) { + processBuilder.command("cmd", + "/c", + "start", + "", + "\"%s\"".formatted(command[0]), + "\"%s\"".formatted(command[1]), + "\"%s\"".formatted(command[2]), + "\"+normal %s|\"".formatted(Integer.toString(column))); + } else if (OS.LINUX) { + processBuilder.command("gnome-terminal", + "--", + command[0], + command[1], + command[2], + command[3]); + } else if (OS.OS_X) { + processBuilder.command("open", + "-a", + "Terminal", + "--args", + command[0], + command[1], + command[2], + command[3]); + } + processBuilder.start(); + } catch (IOException e) { + LOGGER.warn("Problem pushing to Vim.", e); + couldNotCall = true; + } + } + + private boolean determineCommandPath() { + couldNotPush = false; + couldNotCall = false; + notDefined = false; + + commandPath = preferences.getPushToApplicationPreferences().getCommandPaths().get(this.getDisplayName()); + + if ((commandPath == null) || commandPath.trim().isEmpty()) { + notDefined = true; + return false; + } + return true; + } + + @Override + protected String[] jumpToLineCommandlineArguments(Path fileName, int line, int column) { + return new String[] {commandPath, "+%s".formatted(line), fileName.toString(), "+\"normal %s|\"".formatted(column)}; + } } diff --git a/src/main/java/org/jabref/gui/push/PushToWinEdt.java b/src/main/java/org/jabref/gui/push/PushToWinEdt.java index 7545a75244c..e1c8269d7b9 100644 --- a/src/main/java/org/jabref/gui/push/PushToWinEdt.java +++ b/src/main/java/org/jabref/gui/push/PushToWinEdt.java @@ -1,5 +1,7 @@ package org.jabref.gui.push; +import java.nio.file.Path; + import org.jabref.gui.DialogService; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.icon.JabRefIcon; @@ -28,4 +30,9 @@ protected String[] getCommandLine(String keyString) { return new String[] {commandPath, "\"[InsText('" + getCitePrefix() + keyString.replace("'", "''") + getCiteSuffix() + "');]\""}; } + + @Override + protected String[] jumpToLineCommandlineArguments(Path fileName, int line, int column) { + return new String[] {commandPath, "\"[Open(|%s|);SelLine(%s,%s);]\"".formatted(fileName.toString(), line, column)}; + } } diff --git a/src/test/java/org/jabref/gui/push/PushToWinEdtTest.java b/src/test/java/org/jabref/gui/push/PushToWinEdtTest.java new file mode 100644 index 00000000000..3ca3ed7cc30 --- /dev/null +++ b/src/test/java/org/jabref/gui/push/PushToWinEdtTest.java @@ -0,0 +1,30 @@ +package org.jabref.gui.push; + +import java.nio.file.Path; + +import org.jabref.gui.DialogService; +import org.jabref.gui.preferences.GuiPreferences; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Answers; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; + +class PushToWinEdtTest { + + private DialogService dialogService; + private GuiPreferences preferences; + + @BeforeEach + void setup() { + dialogService = mock(DialogService.class, Answers.RETURNS_DEEP_STUBS); + preferences = mock(GuiPreferences.class); + } + + @Test + void jumpToLineCommandlineArguments() { + assertNotNull(new PushToWinEdt(dialogService, preferences).jumpToLineCommandlineArguments(Path.of("test.tex"), 1, 5)); + } +}