SandboxHintHyperlinkDetector.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer - initial API and implementation
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.editor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlink;
/**
* Hyperlink detector for navigating from guard function references in
* DSL code to their definitions in embedded Java ({@code <? ?>}) blocks.
*
* <p>When the user Ctrl+Clicks (or presses F3) on a guard function name
* after a {@code ::} separator, this detector searches for a matching
* method in the document's {@code <? ?>} blocks and navigates to it.</p>
*
* @since 1.5.0
*/
public class SandboxHintHyperlinkDetector extends AbstractHyperlinkDetector {
@Override
public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region,
boolean canShowMultipleHyperlinks) {
IDocument document = textViewer.getDocument();
if (document == null) {
return null;
}
int offset = region.getOffset();
try {
// Check that we're in the default content type (DSL code)
ITypedRegion partition = document.getPartition(offset);
if (!IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
return null;
}
// Extract the word at the cursor
String word = extractWord(document, offset);
if (word == null || word.isEmpty()) {
return null;
}
// Check if the word is preceded by :: (guard reference)
int wordStart = findWordStart(document, offset);
if (!isAfterGuardSeparator(document, wordStart)) {
return null;
}
// Search for a method with this name in <? ?> blocks
IRegion target = findMethodInJavaBlocks(document, word);
if (target == null) {
return null;
}
IRegion linkRegion = new Region(wordStart, word.length());
return new IHyperlink[] {
new GuardFunctionHyperlink(linkRegion, target, word, textViewer)
};
} catch (BadLocationException e) {
return null;
}
}
private String extractWord(IDocument document, int offset) throws BadLocationException {
int start = findWordStart(document, offset);
int end = offset;
while (end < document.getLength()) {
char c = document.getChar(end);
if (!Character.isLetterOrDigit(c) && c != '_') {
break;
}
end++;
}
if (end > start) {
return document.get(start, end - start);
}
return null;
}
private int findWordStart(IDocument document, int offset) throws BadLocationException {
int start = offset;
while (start > 0) {
char c = document.getChar(start - 1);
if (!Character.isLetterOrDigit(c) && c != '_') {
break;
}
start--;
}
return start;
}
private boolean isAfterGuardSeparator(IDocument document, int offset) throws BadLocationException {
int pos = offset - 1;
// Skip whitespace
while (pos >= 0 && Character.isWhitespace(document.getChar(pos))) {
pos--;
}
// Check for '::'
if (pos >= 1
&& document.getChar(pos) == ':'
&& document.getChar(pos - 1) == ':') {
return true;
}
return false;
}
/**
* Searches for a method definition in embedded Java blocks.
*/
private IRegion findMethodInJavaBlocks(IDocument document, String methodName)
throws BadLocationException {
ITypedRegion[] partitions = document.computePartitioning(0, document.getLength());
for (ITypedRegion partition : partitions) {
if (SandboxHintPartitionScanner.JAVA_CODE.equals(partition.getType())) {
String text = document.get(partition.getOffset(), partition.getLength());
// Look for the method name followed by (
int idx = text.indexOf(methodName + "("); //$NON-NLS-1$
if (idx >= 0) {
// Verify it's a method declaration (preceded by a type name)
int checkPos = idx - 1;
while (checkPos >= 0 && Character.isWhitespace(text.charAt(checkPos))) {
checkPos--;
}
if (checkPos >= 0 && (Character.isLetterOrDigit(text.charAt(checkPos))
|| text.charAt(checkPos) == ']')) {
return new Region(partition.getOffset() + idx, methodName.length());
}
}
}
}
return null;
}
/**
* A hyperlink that navigates to a guard function definition.
*/
private static class GuardFunctionHyperlink implements IHyperlink {
private final IRegion linkRegion;
private final IRegion targetRegion;
private final String functionName;
private final ITextViewer viewer;
GuardFunctionHyperlink(IRegion linkRegion, IRegion targetRegion,
String functionName, ITextViewer viewer) {
this.linkRegion = linkRegion;
this.targetRegion = targetRegion;
this.functionName = functionName;
this.viewer = viewer;
}
@Override
public IRegion getHyperlinkRegion() {
return linkRegion;
}
@Override
public String getTypeLabel() {
return "Guard Function"; //$NON-NLS-1$
}
@Override
public String getHyperlinkText() {
return "Open guard function '" + functionName + "'"; //$NON-NLS-1$ //$NON-NLS-2$
}
@Override
public void open() {
viewer.setSelectedRange(targetRegion.getOffset(), targetRegion.getLength());
viewer.revealRange(targetRegion.getOffset(), targetRegion.getLength());
}
}
}