diff --git a/.jbang/JabLsLauncher.java b/.jbang/JabLsLauncher.java index 354cf6b5956..758ccc55144 100755 --- a/.jbang/JabLsLauncher.java +++ b/.jbang/JabLsLauncher.java @@ -18,6 +18,12 @@ //SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/LspDiagnosticBuilder.java //SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/LspDiagnosticHandler.java //SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/LspIntegrityCheck.java +//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/DefinitionProvider.java +//SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProviderFactory.java +//SOURCES ../jabls/src/main/java/org/jabref/languageserver/util/definition/MarkdownDefinitionProvider.java // REPOS mavencentral,snapshots=https://central.sonatype.com/repository/maven-snapshots/ // REPOS mavencentral,mavencentralsnapshots=https://central.sonatype.com/repository/maven-snapshots/,s01oss=https://s01.oss.sonatype.org/content/repositories/snapshots/,oss=https://oss.sonatype.org/content/repositories,jitpack=https://jitpack.io,oss2=https://oss.sonatype.org/content/groups/public,ossrh=https://oss.sonatype.org/content/repositories/snapshots diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java index b96f08b6f30..56ecb163032 100644 --- a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java @@ -424,12 +424,10 @@ private boolean upperRightIsInBounds(CoreGuiPreferences coreGuiPreferences) { // Background tasks public void startBackgroundTasks() { RemotePreferences remotePreferences = preferences.getRemotePreferences(); - + CLIMessageHandler cliMessageHandler = new CLIMessageHandler(mainFrame, preferences); if (remotePreferences.useRemoteServer()) { remoteListenerServerManager.openAndStart( - new CLIMessageHandler( - mainFrame, - preferences), + cliMessageHandler, remotePreferences.getPort()); } @@ -437,7 +435,7 @@ public void startBackgroundTasks() { httpServerManager.start(stateManager, remotePreferences.getHttpServerUri()); } if (remotePreferences.enableLanguageServer()) { - languageServerController.start(remotePreferences.getLanguageServerPort()); + languageServerController.start(cliMessageHandler, remotePreferences.getLanguageServerPort()); } } diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java index aa2f63470b5..1f1daf8d821 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java @@ -285,13 +285,13 @@ public void storeSettings() { }); UiMessageHandler uiMessageHandler = Injector.instantiateModelOrService(UiMessageHandler.class); + CLIMessageHandler messageHandler = new CLIMessageHandler(uiMessageHandler, preferences); RemoteListenerServerManager remoteListenerServerManager = Injector.instantiateModelOrService(RemoteListenerServerManager.class); // stop in all cases, because the port might have changed remoteListenerServerManager.stop(); if (remoteServerProperty.getValue()) { remotePreferences.setUseRemoteServer(true); - remoteListenerServerManager.openAndStart( - new CLIMessageHandler(uiMessageHandler, preferences), + remoteListenerServerManager.openAndStart(messageHandler, remotePreferences.getPort()); } else { remotePreferences.setUseRemoteServer(false); @@ -327,7 +327,7 @@ public void storeSettings() { languageServerController.stop(); if (enableLanguageServerProperty.getValue()) { remotePreferences.setEnableLanguageServer(true); - languageServerController.start(remotePreferences.getLanguageServerPort()); + languageServerController.start(messageHandler, remotePreferences.getLanguageServerPort()); } else { remotePreferences.setEnableLanguageServer(false); languageServerController.stop(); diff --git a/jablib/src/main/java/org/jabref/logic/importer/ParserResult.java b/jablib/src/main/java/org/jabref/logic/importer/ParserResult.java index 3b08b7909ed..5d7863677ca 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/ParserResult.java +++ b/jablib/src/main/java/org/jabref/logic/importer/ParserResult.java @@ -105,11 +105,11 @@ public void setPath(Path path) { * * @param s String Warning text. Must be pre-translated. Only added if there isn't already a dupe. */ - public void addWarning(String s) { + public void addWarning(@NonNull String s) { addWarning(Range.NULL_RANGE, s); } - public void addWarning(Range range, String s) { + public void addWarning(Range range, @NonNull String s) { warnings.put(range, s); } diff --git a/jabls/src/main/java/org/jabref/languageserver/BibtexTextDocumentService.java b/jabls/src/main/java/org/jabref/languageserver/BibtexTextDocumentService.java index cff7be6d182..53962a3dcbc 100644 --- a/jabls/src/main/java/org/jabref/languageserver/BibtexTextDocumentService.java +++ b/jabls/src/main/java/org/jabref/languageserver/BibtexTextDocumentService.java @@ -1,30 +1,56 @@ package org.jabref.languageserver; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import org.jabref.languageserver.util.LspDiagnosticHandler; +import org.jabref.languageserver.util.LspLinkHandler; +import org.jabref.logic.remote.server.RemoteMessageHandler; +import com.google.gson.JsonArray; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentLink; +import org.eclipse.lsp4j.DocumentLinkParams; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.TextDocumentContentChangeEvent; +import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class BibtexTextDocumentService implements TextDocumentService { - private final LspDiagnosticHandler diagnosticHandler; + private static final Logger LOGGER = LoggerFactory.getLogger(BibtexTextDocumentService.class); + private final LspClientHandler clientHandler; + private final LspDiagnosticHandler diagnosticHandler; + private final LspLinkHandler linkHandler; + private final RemoteMessageHandler messageHandler; + private final Map fileUriToLanguageId; + private final Map contentCache; private LanguageClient client; - public BibtexTextDocumentService(@NonNull LspDiagnosticHandler diagnosticHandler) { + public BibtexTextDocumentService(@NonNull RemoteMessageHandler messageHandler, @NonNull LspClientHandler clientHandler, @NonNull LspDiagnosticHandler diagnosticHandler, @NonNull LspLinkHandler linkHandler) { + this.clientHandler = clientHandler; this.diagnosticHandler = diagnosticHandler; + this.linkHandler = linkHandler; + this.fileUriToLanguageId = new ConcurrentHashMap<>(); + this.contentCache = new ConcurrentHashMap<>(); + this.messageHandler = messageHandler; } public void setClient(LanguageClient client) { @@ -33,22 +59,73 @@ public void setClient(LanguageClient client) { @Override public void didOpen(DidOpenTextDocumentParams params) { - diagnosticHandler.computeAndPublishDiagnostics(client, params.getTextDocument().getUri(), params.getTextDocument().getText(), params.getTextDocument().getVersion()); + TextDocumentItem textDocument = params.getTextDocument(); + LOGGER.debug("didOpen {}", textDocument.getUri()); + fileUriToLanguageId.putIfAbsent(textDocument.getUri(), textDocument.getLanguageId()); + + if ("bibtex".equals(textDocument.getLanguageId())) { + diagnosticHandler.computeAndPublishDiagnostics(client, textDocument.getUri(), textDocument.getText(), textDocument.getVersion()); + } else { + contentCache.put(textDocument.getUri(), textDocument.getText()); + } } @Override public void didChange(DidChangeTextDocumentParams params) { - diagnosticHandler.computeAndPublishDiagnostics(client, params.getTextDocument().getUri(), params.getContentChanges().getFirst().getText(), params.getTextDocument().getVersion()); + VersionedTextDocumentIdentifier textDocument = params.getTextDocument(); + TextDocumentContentChangeEvent contentChange = params.getContentChanges().getFirst(); + LOGGER.debug("didChange {}", textDocument.getUri()); + String languageId = fileUriToLanguageId.get(textDocument.getUri()); + + if ("bibtex".equalsIgnoreCase(languageId)) { + diagnosticHandler.computeAndPublishDiagnostics(client, textDocument.getUri(), contentChange.getText(), textDocument.getVersion()); + } else { + contentCache.put(textDocument.getUri(), contentChange.getText()); + } } @Override public void didClose(DidCloseTextDocumentParams params) { + fileUriToLanguageId.remove(params.getTextDocument().getUri()); + contentCache.remove(params.getTextDocument().getUri()); } @Override 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()); + } + return CompletableFuture.completedFuture(Either.forLeft(List.of())); + } + + @Override + public CompletableFuture> 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)); + } + + @Override + public CompletableFuture documentLinkResolve(DocumentLink params) { + if (clientHandler.isStandalone()) { + return CompletableFuture.completedFuture(null); + } + if (params.getData() instanceof JsonArray data) { + messageHandler.handleCommandLineArguments(new String[] {data.asList().getFirst().getAsString(), data.asList().getLast().getAsString()}); + } + return CompletableFuture.completedFuture(null); + } + @Override public CompletableFuture, CompletionList>> completion(CompletionParams position) { return TextDocumentService.super.completion(position); diff --git a/jabls/src/main/java/org/jabref/languageserver/BibtexWorkspaceService.java b/jabls/src/main/java/org/jabref/languageserver/BibtexWorkspaceService.java index 2d3e67066ee..7e72792698e 100644 --- a/jabls/src/main/java/org/jabref/languageserver/BibtexWorkspaceService.java +++ b/jabls/src/main/java/org/jabref/languageserver/BibtexWorkspaceService.java @@ -24,7 +24,6 @@ public BibtexWorkspaceService(LspClientHandler clientHandler, LspDiagnosticHandl this.diagnosticHandler = diagnosticHandler; } - // Todo: handle event @Override public void didChangeConfiguration(DidChangeConfigurationParams didChangeConfigurationParams) { if (didChangeConfigurationParams.getSettings() instanceof JsonObject settings) { diff --git a/jabls/src/main/java/org/jabref/languageserver/LspClientHandler.java b/jabls/src/main/java/org/jabref/languageserver/LspClientHandler.java index 01b719b3132..44708dfcc7f 100644 --- a/jabls/src/main/java/org/jabref/languageserver/LspClientHandler.java +++ b/jabls/src/main/java/org/jabref/languageserver/LspClientHandler.java @@ -3,9 +3,13 @@ import java.util.concurrent.CompletableFuture; import org.jabref.languageserver.util.LspDiagnosticHandler; +import org.jabref.languageserver.util.LspLinkHandler; +import org.jabref.languageserver.util.LspParserHandler; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.preferences.CliPreferences; +import org.jabref.logic.remote.server.RemoteMessageHandler; +import org.eclipse.lsp4j.DocumentLinkOptions; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.MessageParams; @@ -27,17 +31,24 @@ public class LspClientHandler implements LanguageServer, LanguageClientAware { private static final Logger LOGGER = LoggerFactory.getLogger(LspClientHandler.class); private final LspDiagnosticHandler diagnosticHandler; + private final LspParserHandler parserHandler; + private final LspLinkHandler linkHandler; private final BibtexWorkspaceService workspaceService; private final BibtexTextDocumentService textDocumentService; private final ExtensionSettings settings; + private final RemoteMessageHandler messageHandler; private LanguageClient client; + private boolean standalone = false; - public LspClientHandler(CliPreferences cliPreferences, JournalAbbreviationRepository abbreviationRepository) { + public LspClientHandler(RemoteMessageHandler messageHandler, CliPreferences cliPreferences, JournalAbbreviationRepository abbreviationRepository) { this.settings = ExtensionSettings.getDefaultSettings(); - this.diagnosticHandler = new LspDiagnosticHandler(this, cliPreferences, abbreviationRepository); + this.parserHandler = new LspParserHandler(); + this.diagnosticHandler = new LspDiagnosticHandler(this, parserHandler, cliPreferences, abbreviationRepository); + this.linkHandler = new LspLinkHandler(parserHandler); this.workspaceService = new BibtexWorkspaceService(this, diagnosticHandler); - this.textDocumentService = new BibtexTextDocumentService(diagnosticHandler); + this.textDocumentService = new BibtexTextDocumentService(messageHandler, this, diagnosticHandler, linkHandler); + this.messageHandler = messageHandler; } @Override @@ -51,6 +62,11 @@ public CompletableFuture initialize(InitializeParams params) { capabilities.setTextDocumentSync(syncOptions); capabilities.setWorkspace(new WorkspaceServerCapabilities()); + capabilities.setDefinitionProvider(true); + + DocumentLinkOptions linkOptions = new DocumentLinkOptions(); + linkOptions.setResolveProvider(true); + capabilities.setDocumentLinkProvider(linkOptions); return CompletableFuture.completedFuture(new InitializeResult(capabilities)); } @@ -88,4 +104,12 @@ public void connect(LanguageClient client) { textDocumentService.setClient(client); client.logMessage(new MessageParams(MessageType.Warning, "BibtexLSPServer connected.")); } + + public void setStandalone(boolean standalone) { + this.standalone = standalone; + } + + public boolean isStandalone() { + return standalone; + } } diff --git a/jabls/src/main/java/org/jabref/languageserver/LspLauncher.java b/jabls/src/main/java/org/jabref/languageserver/LspLauncher.java index 207f3e83087..ac529429ecf 100644 --- a/jabls/src/main/java/org/jabref/languageserver/LspLauncher.java +++ b/jabls/src/main/java/org/jabref/languageserver/LspLauncher.java @@ -12,8 +12,11 @@ import org.jabref.logic.journals.JournalAbbreviationLoader; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.preferences.CliPreferences; +import org.jabref.logic.preferences.JabRefCliPreferences; +import org.jabref.logic.remote.server.RemoteMessageHandler; import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.launch.LSPLauncher; import org.eclipse.lsp4j.services.LanguageClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,21 +28,29 @@ public class LspLauncher extends Thread { private final CliPreferences cliPreferences; private final JournalAbbreviationRepository abbreviationRepository; private final ExecutorService threadPool; + private final RemoteMessageHandler messageHandler; private final int port; + private boolean standalone = false; private volatile boolean running; private ServerSocket serverSocket; - public LspLauncher(CliPreferences cliPreferences, JournalAbbreviationRepository abbreviationRepository, int port) { + public LspLauncher(RemoteMessageHandler messageHandler, CliPreferences cliPreferences, JournalAbbreviationRepository abbreviationRepository, int port) { this.cliPreferences = cliPreferences; this.abbreviationRepository = abbreviationRepository; this.threadPool = Executors.newCachedThreadPool(); this.port = port; this.setName("JabLs - JabRef Language Server on: " + port); + this.messageHandler = messageHandler; } - public LspLauncher(CliPreferences cliPreferences, int port) { - this(cliPreferences, JournalAbbreviationLoader.loadRepository(cliPreferences.getJournalAbbreviationPreferences()), port); + public LspLauncher(RemoteMessageHandler messageHandler, CliPreferences cliPreferences, int port) { + this(messageHandler, cliPreferences, JournalAbbreviationLoader.loadRepository(cliPreferences.getJournalAbbreviationPreferences()), port); + } + + public LspLauncher(JabRefCliPreferences instance, Integer port) { + this(_ -> LOGGER.warn("LSP cannot handle UICommands in standalone mode."), instance, port); + this.standalone = true; } @Override @@ -69,12 +80,13 @@ public void run() { } private void handleClient(Socket socket) { - LspClientHandler clientHandler = new LspClientHandler(cliPreferences, abbreviationRepository); + LspClientHandler clientHandler = new LspClientHandler(messageHandler, cliPreferences, abbreviationRepository); + clientHandler.setStandalone(standalone); LOGGER.debug("LSP clientHandler started."); try (socket; // socket should be closed on error InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream()) { - Launcher launcher = org.eclipse.lsp4j.launch.LSPLauncher.createServerLauncher(clientHandler, in, out, Executors.newCachedThreadPool(), Function.identity()); + Launcher launcher = LSPLauncher.createServerLauncher(clientHandler, in, out, Executors.newCachedThreadPool(), Function.identity()); LOGGER.debug("LSP clientHandler launched."); clientHandler.connect(launcher.getRemoteProxy()); LOGGER.debug("LSP clientHandler connected."); @@ -105,4 +117,8 @@ public void interrupt() { public boolean isRunning() { return running; } + + public boolean isStandalone() { + return standalone; + } } diff --git a/jabls/src/main/java/org/jabref/languageserver/controller/LanguageServerController.java b/jabls/src/main/java/org/jabref/languageserver/controller/LanguageServerController.java index 84a4992bded..7fe034777a3 100644 --- a/jabls/src/main/java/org/jabref/languageserver/controller/LanguageServerController.java +++ b/jabls/src/main/java/org/jabref/languageserver/controller/LanguageServerController.java @@ -3,6 +3,7 @@ import org.jabref.languageserver.LspLauncher; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.preferences.CliPreferences; +import org.jabref.logic.remote.server.RemoteMessageHandler; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; @@ -24,13 +25,13 @@ public LanguageServerController(CliPreferences cliPreferences, JournalAbbreviati LOGGER.debug("LanguageServerController initialized."); } - public synchronized void start(int port) { + public synchronized void start(RemoteMessageHandler messageHandler, int port) { if (lspLauncher != null) { LOGGER.warn("Language server controller already started, cannot start again."); return; } - lspLauncher = new LspLauncher(cliPreferences, abbreviationRepository, port); + lspLauncher = new LspLauncher(messageHandler, cliPreferences, abbreviationRepository, port); // This enqueues the thread to run in the background // The JVM will take care of running it at some point in time in the future // Thus, we cannot check directly if it really runs diff --git a/jabls/src/main/java/org/jabref/languageserver/util/LspDiagnosticBuilder.java b/jabls/src/main/java/org/jabref/languageserver/util/LspDiagnosticBuilder.java index 6b37f7f9bae..d8199879746 100644 --- a/jabls/src/main/java/org/jabref/languageserver/util/LspDiagnosticBuilder.java +++ b/jabls/src/main/java/org/jabref/languageserver/util/LspDiagnosticBuilder.java @@ -1,14 +1,11 @@ package org.jabref.languageserver.util; -import java.util.Objects; - import org.jabref.logic.importer.ParserResult; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DiagnosticSeverity; -import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -74,7 +71,7 @@ public LspDiagnosticBuilder setRange(Range range) { } public LspDiagnosticBuilder setRange(ParserResult.Range range) { - this.explicitRange = convertToLspRange(range); + this.explicitRange = LspRangeUtil.convertToLspRange(range); return this; } @@ -84,11 +81,9 @@ public LspDiagnosticBuilder setParserResult(ParserResult parserResult) { } public Diagnostic build() { - Objects.requireNonNull(message, "message must be set"); - Range range = explicitRange; if (explicitRange == null) { - range = convertToLspRange(computeRange()); + range = LspRangeUtil.convertToLspRange(computeRange()); } return new Diagnostic(range, message, severity, source); } @@ -104,11 +99,4 @@ private ParserResult.Range computeRange() { return parserResult.getFieldRange(entry, field); } - - private Range convertToLspRange(ParserResult.Range range) { - return new Range( - new Position(Math.max(range.startLine() - 1, 0), Math.max(range.startColumn() - 1, 0)), - new Position(Math.max(range.endLine() - 1, 0), Math.max(range.endColumn() - 1, 0)) - ); - } } diff --git a/jabls/src/main/java/org/jabref/languageserver/util/LspDiagnosticHandler.java b/jabls/src/main/java/org/jabref/languageserver/util/LspDiagnosticHandler.java index 658af8bdc29..fe08f777144 100644 --- a/jabls/src/main/java/org/jabref/languageserver/util/LspDiagnosticHandler.java +++ b/jabls/src/main/java/org/jabref/languageserver/util/LspDiagnosticHandler.java @@ -1,7 +1,6 @@ package org.jabref.languageserver.util; import java.io.IOException; -import java.io.Reader; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -13,9 +12,7 @@ import org.jabref.languageserver.ExtensionSettings; import org.jabref.languageserver.LspClientHandler; import org.jabref.logic.JabRefException; -import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.ParserResult; -import org.jabref.logic.importer.fileformat.BibtexParser; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; import org.jabref.logic.preferences.CliPreferences; @@ -32,18 +29,18 @@ public class LspDiagnosticHandler { private static final Logger LOGGER = LoggerFactory.getLogger(LspDiagnosticHandler.class); private static final int NO_VERSION = -1; - private final CliPreferences jabRefCliPreferences; private final LspIntegrityCheck lspIntegrityCheck; private final LspConsistencyCheck lspConsistencyCheck; private final LspClientHandler clientHandler; + private final LspParserHandler parserHandler; + private final CliPreferences cliPreferences; private final Map> integrityDiagnosticsCache; // Maps file URIs to the corresponding list of integrity diagnostics private final Map> consistencyDiagnosticsCache; // Maps file URIs to the corresponding list of consistency diagnostics - private LanguageClient client; - - public LspDiagnosticHandler(LspClientHandler clientHandler, CliPreferences cliPreferences, JournalAbbreviationRepository abbreviationRepository) { + public LspDiagnosticHandler(LspClientHandler clientHandler, LspParserHandler parserHandler, CliPreferences cliPreferences, JournalAbbreviationRepository abbreviationRepository) { this.clientHandler = clientHandler; - this.jabRefCliPreferences = cliPreferences; + this.parserHandler = parserHandler; + this.cliPreferences = cliPreferences; this.lspIntegrityCheck = new LspIntegrityCheck(cliPreferences, abbreviationRepository); this.lspConsistencyCheck = new LspConsistencyCheck(clientHandler.getSettings()); this.integrityDiagnosticsCache = new ConcurrentHashMap<>(); @@ -70,8 +67,9 @@ public void publishDiagnostics(LanguageClient client, String uri, List computeDiagnostics(String content, String uri) { List diagnostics = new ArrayList<>(); ParserResult parserResult; + try { - parserResult = parserResultFromString(content, jabRefCliPreferences.getImportFormatPreferences()); + parserResult = parserHandler.parserResultFromString(uri, content, cliPreferences.getImportFormatPreferences()); } catch (JabRefException | IOException e) { Diagnostic parseDiagnostic = LspDiagnosticBuilder.create(Localization.lang( "Failed to parse entries.\nThe following error was encountered:\n%0", @@ -117,9 +115,4 @@ public void refreshDiagnostics(LanguageClient client) { publishDiagnostics(client, uri, diagnostics); }); } - - private ParserResult parserResultFromString(String content, ImportFormatPreferences importFormatPreferences) throws JabRefException, IOException { - BibtexParser parser = new BibtexParser(importFormatPreferences); - return parser.parse(Reader.of(content)); - } } diff --git a/jabls/src/main/java/org/jabref/languageserver/util/LspLinkHandler.java b/jabls/src/main/java/org/jabref/languageserver/util/LspLinkHandler.java new file mode 100644 index 00000000000..d32fad25d1d --- /dev/null +++ b/jabls/src/main/java/org/jabref/languageserver/util/LspLinkHandler.java @@ -0,0 +1,46 @@ +package org.jabref.languageserver.util; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.jabref.languageserver.util.definition.DefinitionProvider; +import org.jabref.languageserver.util.definition.DefinitionProviderFactory; + +import org.eclipse.lsp4j.DocumentLink; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LspLinkHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(LspLinkHandler.class); + + private final LspParserHandler parserHandler; + + public LspLinkHandler(LspParserHandler parserHandler) { + this.parserHandler = parserHandler; + } + + public CompletableFuture, List>> provideDefinition(String languageId, String uri, String content, Position position) { + List locations = List.of(); + Optional provider = DefinitionProviderFactory.getDefinitionProvider(parserHandler, languageId); + if (provider.isPresent()) { + locations = provider.get().provideDefinition(content, position); + } + Either, List> toReturn = Either.forLeft(locations); + return CompletableFuture.completedFuture(toReturn); + } + + public CompletableFuture> provideDocumentLinks(String languageId, String content) { + List documentLinks = List.of(); + Optional provider = DefinitionProviderFactory.getDefinitionProvider(parserHandler, languageId); + if (provider.isPresent()) { + documentLinks = provider.get().provideDocumentLinks(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 new file mode 100644 index 00000000000..d0bd8cdbb5c --- /dev/null +++ b/jabls/src/main/java/org/jabref/languageserver/util/LspParserHandler.java @@ -0,0 +1,50 @@ +package org.jabref.languageserver.util; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import org.jabref.logic.JabRefException; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.fileformat.BibtexParser; +import org.jabref.model.entry.BibEntry; + +public class LspParserHandler { + + private final Map parserResults; + + public LspParserHandler() { + this.parserResults = new ConcurrentHashMap<>(); + } + + public ParserResult parserResultFromString(String fileUri, String content, ImportFormatPreferences importFormatPreferences) throws JabRefException, IOException { + BibtexParser parser = new BibtexParser(importFormatPreferences); + ParserResult parserResult = parser.parse(Reader.of(content)); + parserResults.put(fileUri, parserResult); + return parserResult; + } + + public Optional getParserResultForUri(String fileUri) { + return Optional.ofNullable(parserResults.get(fileUri)); + } + + public Map> searchForEntryByCitationKey(String citationKey) { + Map> result = new ConcurrentHashMap<>(); + parserResults.forEach((fileUri, parserResult) -> { + List entries = parserResult.getDatabase().getEntriesByCitationKey(citationKey); + if (!entries.isEmpty()) { + result.put(fileUri, entries); + } + }); + return result; + } + + public boolean citationKeyExists(String citationKey) { + return parserResults.values().stream() + .anyMatch(parserResult -> !parserResult.getDatabase().getEntriesByCitationKey(citationKey).isEmpty()); + } +} diff --git a/jabls/src/main/java/org/jabref/languageserver/util/LspRangeUtil.java b/jabls/src/main/java/org/jabref/languageserver/util/LspRangeUtil.java new file mode 100644 index 00000000000..507fb02f050 --- /dev/null +++ b/jabls/src/main/java/org/jabref/languageserver/util/LspRangeUtil.java @@ -0,0 +1,69 @@ +package org.jabref.languageserver.util; + +import org.jabref.logic.importer.ParserResult; + +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; + +/// Because only postions are supported by the lsp https://github.com/microsoft/language-server-protocol/issues/96 we need to convert back and forth +public class LspRangeUtil { + + public static int toOffset(String content, Position pos) { + int line = Math.max(0, pos.getLine()); + int character = Math.max(0, pos.getCharacter()); + + int currentLine = 0; + int i = 0; + int lineStart = 0; + + while (i < content.length() && currentLine < line) { + char c = content.charAt(i++); + if (c == '\n') { + lineStart = i; + currentLine++; + } + } + + if (currentLine < line) { + return content.length(); + } + + int lineEnd = content.indexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = content.length(); + } + + int offset = lineStart + Math.min(character, Math.max(0, lineEnd - lineStart)); + return Math.max(0, Math.min(content.length(), offset)); + } + + public static Range convertToLspRange(ParserResult.Range range) { + return new Range( + new Position(Math.max(range.startLine() - 1, 0), Math.max(range.startColumn() - 1, 0)), + new Position(Math.max(range.endLine() - 1, 0), Math.max(range.endColumn() - 1, 0)) + ); + } + + public static Range convertToLspRange(String content, int startIndex, int endIndex) { + Position start = convertToLspPosition(content, startIndex); + Position end = convertToLspPosition(content, endIndex); + return new Range(start, end); + } + + public static Position convertToLspPosition(String content, int index) { + int clampedIndex = Math.max(0, Math.min(content.length(), index)); + int line = 0; + int column = 0; + + for (int i = 0; i < clampedIndex; i++) { + if (content.charAt(i) == '\n') { + line++; + column = 0; + } else { + column++; + } + } + + return new Position(line, column); + } +} 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 new file mode 100644 index 00000000000..402b905b4ea --- /dev/null +++ b/jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProvider.java @@ -0,0 +1,132 @@ +package org.jabref.languageserver.util.definition; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jabref.languageserver.util.LspParserHandler; +import org.jabref.languageserver.util.LspRangeUtil; +import org.jabref.logic.importer.ParserResult; +import org.jabref.model.entry.BibEntry; + +import com.google.gson.JsonArray; +import org.eclipse.lsp4j.DocumentLink; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; + +public abstract class DefinitionProvider { + + private static final Pattern CITATION_KEY_CHAR_PATTERN = Pattern.compile("[a-z0-9_\\-:.+]", Pattern.CASE_INSENSITIVE); + private static final Pattern CITATION_KEY_PATTERN = Pattern.compile("@(?[a-z0-9_.+:-]+)", Pattern.CASE_INSENSITIVE); + private static final Pattern VALID_BEFORE_AT = Pattern.compile("[\\s\\[({;,:\\-—–«“\"'’]?"); + + protected final LspParserHandler parserHandler; + + public DefinitionProvider(LspParserHandler parserHandler) { + this.parserHandler = parserHandler; + } + + public List provideDefinition(String content, Position position) { + Optional citationKey = getCitationKeyAtPosition(content, position); + if (citationKey.isPresent()) { + Map> entriesMap = parserHandler.searchForEntryByCitationKey(citationKey.get()); + if (!entriesMap.isEmpty()) { + return entriesMap.entrySet().stream() + .flatMap(listEntry -> listEntry.getValue().stream().map(entry -> new Location(listEntry.getKey(), getRangeFromEntry(listEntry.getKey(), entry)))) + .toList(); + } + } + return List.of(); + } + + public List provideDocumentLinks(String content) { + Matcher matcher = CITATION_KEY_PATTERN.matcher(content); + return matcher.results() + .map(matchResult -> { + String citationKey = matchResult.group("citationkey"); + Range range = LspRangeUtil.convertToLspRange(content, matchResult.start(), matchResult.end()); + DocumentLink documentLink = new DocumentLink(); + documentLink.setRange(range); + JsonArray data = new JsonArray(); + data.add("--jumpToKey"); + data.add(citationKey); + documentLink.setData(data); + return documentLink; + }) + .toList(); + } + + Range getRangeFromEntry(String fileUri, BibEntry entry) { + ParserResult parserResult = parserHandler.getParserResultForUri(fileUri).get(); // always present if we have an entry from provideDefinition + return LspRangeUtil.convertToLspRange(parserResult.getArticleRanges().get(entry)); + } + + Optional getCitationKeyAtPosition(String content, Position position) { + if (content == null || content.isEmpty() || position == null) { + return Optional.empty(); + } + + int around = LspRangeUtil.toOffset(content, position); + if (around < 0 || around > content.length()) { + return Optional.empty(); + } + + int start = lookaheadForCitationKeyStart(content, position); + if (start < 0) { + return Optional.empty(); + } + + int end = lookaheadForCitationKeyEnd(content, position); + if (end < 0 || end <= start) { + return Optional.empty(); + } + + return Optional.of(content.substring(start, end)); + } + + private int lookaheadForCitationKeyEnd(String content, Position position) { + int i = lookaheadForCitationKeyStart(content, position); + while (i < content.length() && isCitationKeyCharacter(content.charAt(i))) { + i++; + } + return i; + } + + private int lookaheadForCitationKeyStart(String content, Position position) { + int caretOffset = clamp(LspRangeUtil.toOffset(content, position), 0, content.length()); + int scanIndex = Math.min(Math.max(0, caretOffset - 1), Math.max(0, content.length() - 1)); + while (scanIndex >= 0 && isCitationKeyCharacter(content.charAt(scanIndex))) { + scanIndex--; + } + + if (scanIndex >= 0 && content.charAt(scanIndex) == '@') { + if (isValidCitationKeyCharBefore(content, scanIndex - 1)) { + return scanIndex + 1; + } + return -1; + } + if (caretOffset < content.length() && content.charAt(caretOffset) == '@' && isValidCitationKeyCharBefore(content, caretOffset - 1)) { + return caretOffset + 1; + } + return -1; + } + + private int clamp(int i, int min, int max) { + return Math.max(min, Math.min(i, max)); + } + + private boolean isCitationKeyCharacter(char c) { + return CITATION_KEY_CHAR_PATTERN.matcher(String.valueOf(c)).matches(); + } + + private boolean isValidCitationKeyCharBefore(String content, int idBeforeAt) { + if (idBeforeAt < 0) { + return true; + } + String before = content.substring(idBeforeAt, idBeforeAt + 1); + return VALID_BEFORE_AT.matcher(before).matches(); + } +} 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 new file mode 100644 index 00000000000..25f220f6da8 --- /dev/null +++ b/jabls/src/main/java/org/jabref/languageserver/util/definition/DefinitionProviderFactory.java @@ -0,0 +1,21 @@ +package org.jabref.languageserver.util.definition; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.jabref.languageserver.util.LspParserHandler; + +public class DefinitionProviderFactory { + + private static final Map PROVIDER_MAP = new HashMap<>(); + + public static Optional getDefinitionProvider(LspParserHandler parserHandler, String languageId) { + return Optional.ofNullable(PROVIDER_MAP.computeIfAbsent(languageId.toLowerCase(), key -> switch (key) { + case "markdown" -> + new MarkdownDefinitionProvider(parserHandler); + default -> + null; + })); + } +} diff --git a/jabls/src/main/java/org/jabref/languageserver/util/definition/MarkdownDefinitionProvider.java b/jabls/src/main/java/org/jabref/languageserver/util/definition/MarkdownDefinitionProvider.java new file mode 100644 index 00000000000..683e829915f --- /dev/null +++ b/jabls/src/main/java/org/jabref/languageserver/util/definition/MarkdownDefinitionProvider.java @@ -0,0 +1,10 @@ +package org.jabref.languageserver.util.definition; + +import org.jabref.languageserver.util.LspParserHandler; + +public class MarkdownDefinitionProvider extends DefinitionProvider { + + public MarkdownDefinitionProvider(LspParserHandler parserHandler) { + super(parserHandler); + } +}