diff --git a/.github/workflows/tests-code.yml b/.github/workflows/tests-code.yml index 5cdaa367392..2db3740d367 100644 --- a/.github/workflows/tests-code.yml +++ b/.github/workflows/tests-code.yml @@ -572,6 +572,7 @@ jobs: esac echo "//SOURCES ../$f" >> "${{ matrix.script }}" done + - run: cat ${{ matrix.script }} - run: jbang build "${{ matrix.script }}" shell: bash - run: jbang "${{ matrix.script }}" --help diff --git a/.jbang/JabLsLauncher.java b/.jbang/JabLsLauncher.java index 518a749107a..a5e6f532539 100755 --- a/.jbang/JabLsLauncher.java +++ b/.jbang/JabLsLauncher.java @@ -21,6 +21,7 @@ //SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/LspLinkHandler.java //SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/LspParserHandler.java //SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/LspRangeUtil.java +//SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/definition/BibDefinitionProvider.java //SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProvider.java //SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProviderFactory.java //SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/definition/LatexDefinitionProvider.java diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java index 5dee7354cac..30ec90d5b60 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java @@ -88,6 +88,8 @@ /// /// **Opposite class:** /// [`BibDatabaseWriter`](org.jabref.logic.exporter.BibDatabaseWriter) +/// +/// FIXME: This class relies on `char`, but should use [java.lang.Character] to be fully Unicode compliant. public class BibtexParser implements Parser { private static final Logger LOGGER = LoggerFactory.getLogger(BibtexParser.class); private static final int LOOKAHEAD = 1024; @@ -759,7 +761,7 @@ private void parseField(BibEntry entry) throws IOException { Field field = FieldFactory.parseField(parseTextToken()); skipWhitespace(); - consume('='); + consume(field, '='); String content = parseFieldContent(field); if (!content.isEmpty()) { if (entry.hasField(field)) { @@ -1172,6 +1174,19 @@ private void consume(char expected) throws IOException { } } + private void consume(Field field, char expected) throws IOException { + int character = read(); + + if (character != expected) { + throw new IOException( + "Error at line " + line + + " after column " + column + + " (" + field.getName() + "): Expected " + + expected + " but received " + + (char) character + " (" + character + ")"); + } + } + private boolean consumeUncritically(char expected) throws IOException { int character; // @formatter:off diff --git a/jablib/src/main/java/org/jabref/logic/importer/util/FileFieldParser.java b/jablib/src/main/java/org/jabref/logic/importer/util/FileFieldParser.java index 983b0db8a58..8d11b855880 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/util/FileFieldParser.java +++ b/jablib/src/main/java/org/jabref/logic/importer/util/FileFieldParser.java @@ -4,10 +4,14 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.jabref.logic.util.URLUtil; import org.jabref.model.entry.LinkedFile; +import org.jabref.model.util.Range; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,7 +25,7 @@ public class FileFieldParser { private boolean windowsPath; - public FileFieldParser(String value) { + private FileFieldParser(String value) { if (value == null) { this.value = null; } else { @@ -46,11 +50,16 @@ public FileFieldParser(String value) { public static List parse(String value) { // We need state to have a more clean code. Thus, we instantiate the class and then return the result FileFieldParser fileFieldParser = new FileFieldParser(value); - return fileFieldParser.parse(); + return fileFieldParser.parse().stream().map(LinkedFilePosition::linkedFile).collect(Collectors.toList()); } - public List parse() { - List files = new ArrayList<>(); + public static Map parseToPosition(String value) { + FileFieldParser fileFieldParser = new FileFieldParser(value); + return fileFieldParser.parse().stream().collect(HashMap::new, (map, position) -> map.put(position.linkedFile(), position.range()), HashMap::putAll); + } + + private List parse() { + List files = new ArrayList<>(); if ((value == null) || value.trim().isEmpty()) { return files; @@ -59,7 +68,7 @@ public List parse() { if (LinkedFile.isOnlineLink(value.trim())) { // needs to be modifiable try { - return List.of(new LinkedFile(URLUtil.create(value), "")); + return List.of(new LinkedFilePosition(new LinkedFile(URLUtil.create(value), ""), new Range(0, value.length() - 1))); } catch (MalformedURLException e) { LOGGER.error("invalid url", e); return files; @@ -72,6 +81,7 @@ public List parse() { resetDataStructuresForNextElement(); boolean inXmlChar = false; boolean escaped = false; + int startColumn = 0; for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); @@ -114,7 +124,8 @@ public List parse() { } } else if (!escaped && (c == ';') && !inXmlChar) { linkedFileData.add(charactersOfCurrentElement.toString()); - files.add(convert(linkedFileData)); + files.add(new LinkedFilePosition(convert(linkedFileData), new Range(startColumn, i))); + startColumn = i + 1; // next iteration resetDataStructuresForNextElement(); @@ -127,7 +138,7 @@ public List parse() { linkedFileData.add(charactersOfCurrentElement.toString()); } if (!linkedFileData.isEmpty()) { - files.add(convert(linkedFileData)); + files.add(new LinkedFilePosition(convert(linkedFileData), new Range(startColumn, value.length() - 1))); } return files; } @@ -193,4 +204,7 @@ static LinkedFile convert(List entry) { entry.clear(); return field; } + + private record LinkedFilePosition(LinkedFile linkedFile, Range range) { + } } diff --git a/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java b/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java index 16f7c9ce114..52fa90fa3b9 100644 --- a/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java +++ b/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java @@ -403,9 +403,9 @@ public static Optional findSingleFileRecursively(String filename, Path roo return Optional.empty(); } - public static Optional find(final BibDatabaseContext databaseContext, + public static Optional find(@NonNull BibDatabaseContext databaseContext, @NonNull String fileName, - FilePreferences filePreferences) { + @NonNull FilePreferences filePreferences) { return find(fileName, databaseContext.getFileDirectories(filePreferences)); } @@ -416,7 +416,7 @@ public static Optional find(final BibDatabaseContext databaseContext, * Will look in each of the given directories starting from the beginning and * returning the first found file to match if any. */ - public static Optional find(String fileName, List directories) { + public static Optional find(@NonNull String fileName, @NonNull List<@NonNull Path> directories) { if (directories.isEmpty()) { // Fallback, if no directories to resolve are passed Path path = Path.of(fileName); diff --git a/jablib/src/main/java/org/jabref/model/database/BibDatabaseContext.java b/jablib/src/main/java/org/jabref/model/database/BibDatabaseContext.java index d031935936b..e2fc3a75705 100644 --- a/jablib/src/main/java/org/jabref/model/database/BibDatabaseContext.java +++ b/jablib/src/main/java/org/jabref/model/database/BibDatabaseContext.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.SequencedCollection; import java.util.UUID; import org.jabref.architecture.AllowedToUseLogic; @@ -167,9 +168,9 @@ public boolean isStudy() { * @param preferences The fileDirectory preferences * @return List of existing absolute paths */ - public List getFileDirectories(FilePreferences preferences) { + public @NonNull List<@NonNull Path> getFileDirectories(@NonNull FilePreferences preferences) { // Paths are a) ordered and b) should be contained only once in the result - LinkedHashSet fileDirs = new LinkedHashSet<>(3); + SequencedCollection fileDirs = new LinkedHashSet<>(3); Optional userFileDirectory = metaData.getUserFileDirectory(preferences.getUserAndHost()).map(this::getFileDirectoryPath); userFileDirectory.ifPresent(fileDirs::add); diff --git a/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java b/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java index e7df19edc6c..4b74ae770a2 100644 --- a/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java +++ b/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java @@ -28,10 +28,9 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -/** - * Represents the link to an external file (e.g. associated PDF file). - * This class is {@link Serializable} which is needed for drag and drop in gui - */ +/// Represents the link to an external file (e.g. associated PDF file). +/// This class is {@link Serializable} which is needed for drag and drop in gui +/// The conversion from String ([org.jabref.model.entry.field.StandardField.FILE]) is done at [org.jabref.logic.importer.util.FileFieldParser#parse(String)] @AllowedToUseLogic("Uses FileUtil from logic") @NullMarked public class LinkedFile implements Serializable { diff --git a/jablib/src/test/java/org/jabref/logic/importer/ParserResultTest.java b/jablib/src/test/java/org/jabref/logic/importer/ParserResultTest.java index ff32092d9a1..4fa645cee8f 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/ParserResultTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/ParserResultTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; +// Other tests for reading can be found at [org.jabref.logic.importer.fileformat.BibtexImporterTest] class ParserResultTest { @Test void isEmptyForNewParseResult() { diff --git a/jablib/src/test/java/org/jabref/logic/importer/util/FileFieldParserTest.java b/jablib/src/test/java/org/jabref/logic/importer/util/FileFieldParserTest.java index cede695444d..d535ed27d74 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/util/FileFieldParserTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/util/FileFieldParserTest.java @@ -219,6 +219,11 @@ private static Stream stringsToParseTest() throws MalformedURLExcepti Arguments.of( List.of(new LinkedFile("", "A:\\Zotero\\storage\\test.pdf", "")), "A:\\Zotero\\storage\\test.pdf" + ), + // Mixed path + Arguments.of( + List.of(new LinkedFile("", "C:/Users/Philip/Downloads/corti-et-al-2009-cocoa-and-cardiovascular-health.pdf", "")), + ":C\\\\:/Users/Philip/Downloads/corti-et-al-2009-cocoa-and-cardiovascular-health.pdf:PDF" ) ); } diff --git a/jabls/build.gradle.kts b/jabls/build.gradle.kts index cb0e1940d68..26fb242ce5c 100644 --- a/jabls/build.gradle.kts +++ b/jabls/build.gradle.kts @@ -18,10 +18,13 @@ dependencies { // route all requests to java.util.logging to SLF4J (which in turn routes to tinylog) testImplementation("org.slf4j:jul-to-slf4j") + + testImplementation("org.mockito:mockito-core") } javaModuleTesting.whitebox(testing.suites["test"]) { requires.add("org.junit.jupiter.api") + requires.add("org.mockito") } tasks.test { diff --git a/jabls/src/main/java/org/jabref/languageserver/BibtexTextDocumentService.java b/jabls/src/main/java/org/jabref/languageserver/BibtexTextDocumentService.java index 53962a3dcbc..e30f0cda764 100644 --- a/jabls/src/main/java/org/jabref/languageserver/BibtexTextDocumentService.java +++ b/jabls/src/main/java/org/jabref/languageserver/BibtexTextDocumentService.java @@ -65,9 +65,8 @@ public void didOpen(DidOpenTextDocumentParams params) { if ("bibtex".equals(textDocument.getLanguageId())) { diagnosticHandler.computeAndPublishDiagnostics(client, textDocument.getUri(), textDocument.getText(), textDocument.getVersion()); - } else { - contentCache.put(textDocument.getUri(), textDocument.getText()); } + contentCache.put(textDocument.getUri(), textDocument.getText()); } @Override @@ -79,9 +78,8 @@ public void didChange(DidChangeTextDocumentParams params) { if ("bibtex".equalsIgnoreCase(languageId)) { diagnosticHandler.computeAndPublishDiagnostics(client, textDocument.getUri(), contentChange.getText(), textDocument.getVersion()); - } else { - contentCache.put(textDocument.getUri(), contentChange.getText()); } + contentCache.put(textDocument.getUri(), contentChange.getText()); } @Override @@ -96,9 +94,6 @@ public void didSave(DidSaveTextDocumentParams params) { @Override public CompletableFuture, List>> definition(DefinitionParams params) { - if (!clientHandler.isStandalone()) { - return CompletableFuture.completedFuture(Either.forLeft(List.of())); - } if (fileUriToLanguageId.containsKey(params.getTextDocument().getUri())) { String fileUri = params.getTextDocument().getUri(); return linkHandler.provideDefinition(fileUriToLanguageId.get(fileUri), fileUri, contentCache.get(fileUri), params.getPosition()); @@ -108,11 +103,8 @@ public CompletableFuture, List> documentLink(DocumentLinkParams params) { - if (clientHandler.isStandalone()) { - return CompletableFuture.completedFuture(List.of()); - } String fileUri = params.getTextDocument().getUri(); - return linkHandler.provideDocumentLinks(fileUriToLanguageId.get(fileUri), contentCache.get(fileUri)); + return linkHandler.provideDocumentLinks(fileUri, fileUriToLanguageId.get(fileUri), contentCache.get(fileUri)); } @Override diff --git a/jabls/src/main/java/org/jabref/languageserver/LspClientHandler.java b/jabls/src/main/java/org/jabref/languageserver/LspClientHandler.java index 44708dfcc7f..4121b8011c1 100644 --- a/jabls/src/main/java/org/jabref/languageserver/LspClientHandler.java +++ b/jabls/src/main/java/org/jabref/languageserver/LspClientHandler.java @@ -45,7 +45,7 @@ public LspClientHandler(RemoteMessageHandler messageHandler, CliPreferences cliP this.settings = ExtensionSettings.getDefaultSettings(); this.parserHandler = new LspParserHandler(); this.diagnosticHandler = new LspDiagnosticHandler(this, parserHandler, cliPreferences, abbreviationRepository); - this.linkHandler = new LspLinkHandler(parserHandler); + this.linkHandler = new LspLinkHandler(this, parserHandler, cliPreferences.getFilePreferences()); this.workspaceService = new BibtexWorkspaceService(this, diagnosticHandler); this.textDocumentService = new BibtexTextDocumentService(messageHandler, this, diagnosticHandler, linkHandler); this.messageHandler = messageHandler; diff --git a/jabls/src/main/java/org/jabref/languageserver/util/LspIntegrityCheck.java b/jabls/src/main/java/org/jabref/languageserver/util/LspIntegrityCheck.java index 582801766d4..301ab9c4fe4 100644 --- a/jabls/src/main/java/org/jabref/languageserver/util/LspIntegrityCheck.java +++ b/jabls/src/main/java/org/jabref/languageserver/util/LspIntegrityCheck.java @@ -1,6 +1,7 @@ package org.jabref.languageserver.util; import java.util.List; +import java.util.stream.Stream; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.integrity.IntegrityCheck; @@ -8,9 +9,12 @@ import org.jabref.logic.preferences.CliPreferences; import org.eclipse.lsp4j.Diagnostic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class LspIntegrityCheck { + private static final Logger LOGGER = LoggerFactory.getLogger(LspIntegrityCheck.class); private static final boolean ALLOW_INTEGER_EDITION = true; private final CliPreferences cliPreferences; @@ -30,12 +34,19 @@ public List check(ParserResult parserResult) { ALLOW_INTEGER_EDITION ); - return parserResult.getDatabaseContext().getEntries().stream().flatMap(entry -> integrityCheck.checkEntry(entry).stream().map(message -> { - if (entry.getFieldOrAlias(message.field()).isPresent()) { - return LspDiagnosticBuilder.create(parserResult, message.message()).setField(message.field()).setEntry(entry).build(); - } else { - return LspDiagnosticBuilder.create(parserResult, message.message()).setEntry(entry).build(); + return parserResult.getDatabaseContext().getEntries().stream().flatMap(entry -> { + try { + return integrityCheck.checkEntry(entry).stream().map(message -> { + if (entry.getFieldOrAlias(message.field()).isPresent()) { + return LspDiagnosticBuilder.create(parserResult, message.message()).setField(message.field()).setEntry(entry).build(); + } else { + return LspDiagnosticBuilder.create(parserResult, message.message()).setEntry(entry).build(); + } + }); + } catch (NullPointerException nullPointerException) { + LOGGER.debug("Error while performing integrity check.", nullPointerException); } - })).toList(); + return Stream.of(); + }).toList(); } } diff --git a/jabls/src/main/java/org/jabref/languageserver/util/LspLinkHandler.java b/jabls/src/main/java/org/jabref/languageserver/util/LspLinkHandler.java index d32fad25d1d..91946c1f41f 100644 --- a/jabls/src/main/java/org/jabref/languageserver/util/LspLinkHandler.java +++ b/jabls/src/main/java/org/jabref/languageserver/util/LspLinkHandler.java @@ -4,8 +4,10 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; +import org.jabref.languageserver.LspClientHandler; import org.jabref.languageserver.util.definition.DefinitionProvider; import org.jabref.languageserver.util.definition.DefinitionProviderFactory; +import org.jabref.logic.FilePreferences; import org.eclipse.lsp4j.DocumentLink; import org.eclipse.lsp4j.Location; @@ -19,27 +21,38 @@ public class LspLinkHandler { private static final Logger LOGGER = LoggerFactory.getLogger(LspLinkHandler.class); + private final LspClientHandler clientHandler; private final LspParserHandler parserHandler; + private final FilePreferences preferences; - public LspLinkHandler(LspParserHandler parserHandler) { + public LspLinkHandler(LspClientHandler clientHandler, LspParserHandler parserHandler, FilePreferences preferences) { + this.clientHandler = clientHandler; this.parserHandler = parserHandler; + this.preferences = preferences; } public CompletableFuture, List>> provideDefinition(String languageId, String uri, String content, Position position) { + if (!clientHandler.isStandalone() && !"bibtex".equals(languageId)) { + return CompletableFuture.completedFuture(Either.forLeft(List.of())); + } + List locations = List.of(); - Optional provider = DefinitionProviderFactory.getDefinitionProvider(parserHandler, languageId); + Optional provider = DefinitionProviderFactory.getDefinitionProvider(preferences, parserHandler, languageId); if (provider.isPresent()) { - locations = provider.get().provideDefinition(content, position); + locations = provider.get().provideDefinition(uri, content, position); } Either, List> toReturn = Either.forLeft(locations); return CompletableFuture.completedFuture(toReturn); } - public CompletableFuture> provideDocumentLinks(String languageId, String content) { + public CompletableFuture> provideDocumentLinks(String fileUri, String languageId, String content) { + if (clientHandler.isStandalone()) { + return CompletableFuture.completedFuture(List.of()); + } List documentLinks = List.of(); - Optional provider = DefinitionProviderFactory.getDefinitionProvider(parserHandler, languageId); + Optional provider = DefinitionProviderFactory.getDefinitionProvider(preferences, parserHandler, languageId); if (provider.isPresent()) { - documentLinks = provider.get().provideDocumentLinks(content); + documentLinks = provider.get().provideDocumentLinks(fileUri, content); } return CompletableFuture.completedFuture(documentLinks); } diff --git a/jabls/src/main/java/org/jabref/languageserver/util/LspParserHandler.java b/jabls/src/main/java/org/jabref/languageserver/util/LspParserHandler.java index d0bd8cdbb5c..7d6e29bb53c 100644 --- a/jabls/src/main/java/org/jabref/languageserver/util/LspParserHandler.java +++ b/jabls/src/main/java/org/jabref/languageserver/util/LspParserHandler.java @@ -2,6 +2,9 @@ import java.io.IOException; import java.io.Reader; +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.Optional; @@ -22,8 +25,13 @@ public LspParserHandler() { } public ParserResult parserResultFromString(String fileUri, String content, ImportFormatPreferences importFormatPreferences) throws JabRefException, IOException { + // We use BibtexParser directly, because we do not want to add an extra DummyFileMonitor + // Otherwise, we could use `OpenDatabase.loadDatabase(path, importFormatPreferences, new DummyFileUpdateMonitor())` BibtexParser parser = new BibtexParser(importFormatPreferences); ParserResult parserResult = parser.parse(Reader.of(content)); + URI uri = URI.create(fileUri); + Path path = Paths.get(uri); + parserResult.getDatabaseContext().setDatabasePath(path); parserResults.put(fileUri, parserResult); return parserResult; } diff --git a/jabls/src/main/java/org/jabref/languageserver/util/definition/BibDefinitionProvider.java b/jabls/src/main/java/org/jabref/languageserver/util/definition/BibDefinitionProvider.java new file mode 100644 index 00000000000..99be04a1e24 --- /dev/null +++ b/jabls/src/main/java/org/jabref/languageserver/util/definition/BibDefinitionProvider.java @@ -0,0 +1,93 @@ +package org.jabref.languageserver.util.definition; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.jabref.languageserver.util.LspParserHandler; +import org.jabref.languageserver.util.LspRangeUtil; +import org.jabref.logic.FilePreferences; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.util.FileFieldParser; +import org.jabref.logic.util.io.FileUtil; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.model.entry.field.StandardField; + +import org.eclipse.lsp4j.DocumentLink; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.jspecify.annotations.NullMarked; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@NullMarked +public class BibDefinitionProvider extends DefinitionProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(BibDefinitionProvider.class); + private static final Range EMPTY_RANGE = new Range(new Position(0, 0), new Position(0, 0)); + + private final FilePreferences preferences; + + public BibDefinitionProvider(FilePreferences preferences, LspParserHandler parserHandler) { + super(parserHandler); + this.preferences = preferences; + } + + @Override + public List provideDefinition(String uri, String content, Position position) { + Optional parserResultOpt = parserHandler.getParserResultForUri(uri); + if (parserResultOpt.isEmpty()) { + return List.of(); + } + + ParserResult parserResult = parserResultOpt.get(); + + for (Map.Entry entry : parserResult.getArticleRanges().entrySet()) { + BibEntry bibEntry = entry.getKey(); + ParserResult.Range range = entry.getValue(); + if (bibEntry.getField(StandardField.FILE).isPresent() && LspRangeUtil.isPositionInRange(position, LspRangeUtil.convertToLspRange(range))) { + Range fileFieldRange = LspRangeUtil.convertToLspRange(parserResult.getFieldRange(bibEntry, StandardField.FILE)); + if (!LspRangeUtil.isPositionInRange(position, fileFieldRange)) { + return List.of(); + } + Optional fileString = bibEntry.getFieldOrAlias(StandardField.FILE); + if (fileString.isEmpty()) { + return List.of(); + } + + int offsetStart = LspRangeUtil.toOffset(content, fileFieldRange.getStart()); + int offsetEnd = LspRangeUtil.toOffset(content, fileFieldRange.getEnd()); + String fileField = content.substring(offsetStart, offsetEnd); + int startIndex = offsetStart + fileField.indexOf(fileString.get()); + + Map fileRangeMap = FileFieldParser.parseToPosition(fileString.get()); + for (Map.Entry linkedFileRangeEntry : fileRangeMap.entrySet()) { + LinkedFile linkedFile = linkedFileRangeEntry.getKey(); + org.jabref.model.util.Range rangeInFileString = linkedFileRangeEntry.getValue(); + int start = startIndex + rangeInFileString.start(); + int end = start + rangeInFileString.end(); + Range linkRange = LspRangeUtil.convertToLspRange(content, start, end); + if (LspRangeUtil.isPositionInRange(position, linkRange)) { + Optional filePath = FileUtil.find(parserResult.getDatabaseContext(), linkedFile.getLink(), preferences); + if (LOGGER.isDebugEnabled() && filePath.isEmpty()) { + LOGGER.debug("filePath is empty"); + } + return filePath + .map(p -> List.of(new Location(p.toUri().toString(), EMPTY_RANGE))) + .orElse(List.of()); + } + } + } + } + return List.of(); + } + + // Not needed when trying to resolve links to pdfs + @Override + public List provideDocumentLinks(String fileUri, String content) { + return List.of(); + } +} diff --git a/jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProvider.java b/jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProvider.java index 2605c142919..ef56e657539 100644 --- a/jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProvider.java +++ b/jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProvider.java @@ -29,7 +29,7 @@ public DefinitionProvider(LspParserHandler parserHandler) { this.parserHandler = parserHandler; } - public List provideDefinition(String content, Position position) { + public List provideDefinition(String uri, String content, Position position) { Optional citationKey = getCitationKeyAtPosition(content, position); if (citationKey.isPresent()) { Map> entriesMap = parserHandler.searchForEntryByCitationKey(citationKey.get()); @@ -43,7 +43,7 @@ public List provideDefinition(String content, Position position) { return List.of(); } - public List provideDocumentLinks(String content) { + public List provideDocumentLinks(String fileUri, String content) { if (content == null || content.isEmpty()) { return List.of(); } diff --git a/jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProviderFactory.java b/jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProviderFactory.java index 4f8af0043d4..34d33d185dc 100644 --- a/jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProviderFactory.java +++ b/jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProviderFactory.java @@ -5,17 +5,20 @@ import java.util.Optional; import org.jabref.languageserver.util.LspParserHandler; +import org.jabref.logic.FilePreferences; public class DefinitionProviderFactory { private static final Map PROVIDER_MAP = new HashMap<>(); - public static Optional getDefinitionProvider(LspParserHandler parserHandler, String languageId) { + public static Optional getDefinitionProvider(FilePreferences preferences, LspParserHandler parserHandler, String languageId) { return Optional.ofNullable(PROVIDER_MAP.computeIfAbsent(languageId.toLowerCase(), key -> switch (key) { case "markdown" -> new MarkdownDefinitionProvider(parserHandler); case "latex" -> new LatexDefinitionProvider(parserHandler); + case "bibtex" -> + new BibDefinitionProvider(preferences, parserHandler); default -> null; })); diff --git a/jabls/src/main/java/org/jabref/languageserver/util/definition/LatexDefinitionProvider.java b/jabls/src/main/java/org/jabref/languageserver/util/definition/LatexDefinitionProvider.java index d5fd8df3cfa..1b42cd82392 100644 --- a/jabls/src/main/java/org/jabref/languageserver/util/definition/LatexDefinitionProvider.java +++ b/jabls/src/main/java/org/jabref/languageserver/util/definition/LatexDefinitionProvider.java @@ -23,13 +23,13 @@ public LatexDefinitionProvider(LspParserHandler parserHandler) { } @Override - public List provideDefinition(String content, Position position) { + public List provideDefinition(String uri, String content, Position position) { List locations = new ArrayList<>(); latexParser.parse(content).getCitations().forEach((key, citation) -> { Range range = LspRangeUtil.convertToLspRange(citation.line(), citation.colStart(), citation.colEnd()); if (LspRangeUtil.isPositionInRange(position, range)) { - parserHandler.searchForEntryByCitationKey(key).forEach((uri, entries) -> entries.forEach(entry -> { - locations.add(createLocation(getRangeFromEntry(uri, entry), uri)); + parserHandler.searchForEntryByCitationKey(key).forEach((databaseUri, entries) -> entries.forEach(entry -> { + locations.add(createLocation(getRangeFromEntry(databaseUri, entry), databaseUri)); })); } }); @@ -37,7 +37,7 @@ public List provideDefinition(String content, Position position) { } @Override - public List provideDocumentLinks(String content) { + public List provideDocumentLinks(String fileUri, String content) { List locations = new ArrayList<>(); latexParser.parse(content).getCitations().forEach((key, citation) -> { locations.add(createDocumentLink(LspRangeUtil.convertToLspRange(citation.line(), citation.colStart(), citation.colEnd()), key)); diff --git a/jabls/src/test/java/org/jabref/languageserver/util/definition/BibDefinitionProviderTest.java b/jabls/src/test/java/org/jabref/languageserver/util/definition/BibDefinitionProviderTest.java new file mode 100644 index 00000000000..e9884493646 --- /dev/null +++ b/jabls/src/test/java/org/jabref/languageserver/util/definition/BibDefinitionProviderTest.java @@ -0,0 +1,61 @@ +package org.jabref.languageserver.util.definition; + +import java.io.IOException; +import java.util.List; + +import org.jabref.languageserver.util.LspParserHandler; +import org.jabref.logic.FilePreferences; +import org.jabref.logic.JabRefException; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParserResult; +import org.jabref.model.entry.BibEntryPreferences; +import org.jabref.model.entry.LinkedFile; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class BibDefinitionProviderTest { + + private LspParserHandler lspParserHandler = new LspParserHandler(); + private ImportFormatPreferences importFormatPreferences; + + @BeforeEach + void setUp() { + importFormatPreferences = mock(ImportFormatPreferences.class); + when(importFormatPreferences.bibEntryPreferences()).thenReturn(mock(BibEntryPreferences.class)); + when(importFormatPreferences.bibEntryPreferences().getKeywordSeparator()).thenReturn(','); + when(importFormatPreferences.filePreferences()).thenReturn(mock(FilePreferences.class)); + when(importFormatPreferences.filePreferences().getUserAndHost()).thenReturn("MockedUser-mockedhost"); + } + + @Test + void provideDefinition() throws JabRefException, IOException { + ParserResult parserResult = lspParserHandler.parserResultFromString( + "some-uri", + """ + @Article{Cooper_2007, + author = {Cooper, Karen A. and Donovan, Jennifer L. and Waterhouse, Andrew L. and Williamson, Gary}, + date = {2007-08}, + journaltitle = {British Journal of Nutrition}, + title = {Cocoa and health: a decade of research}, + doi = {10.1017/s0007114507795296}, + issn = {1475-2662}, + number = {1}, + pages = {1--11}, + volume = {99}, + file = {:C\\:/Users/Philip/Downloads/corti-et-al-2009-cocoa-and-cardiovascular-health.pdf:PDF;:corti-et-al-2009-cocoa-and-cardiovascular-health.pdf:PDF}, + publisher = {Cambridge University Press (CUP)} + , + } + """, + importFormatPreferences); + List files = parserResult.getDatabaseContext().getEntries().getFirst().getFiles(); + assertEquals(2, files.size()); + assertNotNull(files.getLast().getLink()); + } +}