SandboxHintOutlinePage.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 java.util.ArrayList;
import java.util.List;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.views.contentoutline.ContentOutlinePage;
/**
* Outline view for {@code .sandbox-hint} files showing the document structure.
*
* <p>The outline tree displays:</p>
* <ul>
* <li>Transformation rules (pattern → replacement)</li>
* <li>Embedded Java blocks with their guard/fix methods</li>
* <li>Metadata directives</li>
* </ul>
*
* <p>Clicking an outline element selects and reveals the corresponding
* region in the editor.</p>
*
* @since 1.5.0
*/
public class SandboxHintOutlinePage extends ContentOutlinePage {
private final ITextEditor editor;
/**
* Creates a new outline page for the given editor.
*
* @param editor the text editor this outline belongs to
*/
public SandboxHintOutlinePage(ITextEditor editor) {
this.editor = editor;
}
@Override
public void createControl(Composite parent) {
super.createControl(parent);
TreeViewer viewer = getTreeViewer();
viewer.setContentProvider(new OutlineContentProvider());
viewer.setLabelProvider(new OutlineLabelProvider());
viewer.addSelectionChangedListener(this::handleSelection);
update();
}
/**
* Refreshes the outline content from the current document.
*/
public void update() {
TreeViewer viewer = getTreeViewer();
if (viewer != null) {
Control control = viewer.getControl();
if (control != null && !control.isDisposed()) {
IDocument document = getDocument();
if (document != null) {
viewer.setInput(document);
}
}
}
}
private IDocument getDocument() {
IEditorInput input = editor.getEditorInput();
if (input != null && editor.getDocumentProvider() != null) {
return editor.getDocumentProvider().getDocument(input);
}
return null;
}
private void handleSelection(SelectionChangedEvent event) {
if (event.getSelection() instanceof TreeSelection treeSelection) {
Object element = treeSelection.getFirstElement();
if (element instanceof OutlineElement outlineElement) {
editor.selectAndReveal(outlineElement.offset(), outlineElement.length());
}
}
}
/**
* An element in the outline tree.
*
* @param label the display label
* @param type the element type
* @param offset the character offset in the document
* @param length the character length
* @param children child elements
*/
record OutlineElement(String label, ElementType type, int offset, int length,
List<OutlineElement> children) {
}
/**
* The type of outline element.
*/
enum ElementType {
/** A transformation rule */
RULE,
/** An embedded Java block */
JAVA_BLOCK,
/** A metadata directive */
METADATA,
/** A comment */
COMMENT
}
/**
* Content provider that parses the document into outline elements.
*/
private static class OutlineContentProvider implements ITreeContentProvider {
@Override
public Object[] getElements(Object inputElement) {
if (!(inputElement instanceof IDocument document)) {
return new Object[0];
}
return buildOutline(document).toArray();
}
@Override
public Object[] getChildren(Object parentElement) {
if (parentElement instanceof OutlineElement element && element.children() != null) {
return element.children().toArray();
}
return new Object[0];
}
@Override
public Object getParent(Object element) {
return null;
}
@Override
public boolean hasChildren(Object element) {
return element instanceof OutlineElement oe
&& oe.children() != null && !oe.children().isEmpty();
}
private List<OutlineElement> buildOutline(IDocument document) {
List<OutlineElement> elements = new ArrayList<>();
try {
ITypedRegion[] partitions = document.computePartitioning(0, document.getLength());
int ruleIndex = 0;
for (ITypedRegion partition : partitions) {
String type = partition.getType();
int offset = partition.getOffset();
int length = partition.getLength();
if (SandboxHintPartitionScanner.JAVA_CODE.equals(type)) {
String text = document.get(offset, length);
String label = buildJavaBlockLabel(text);
elements.add(new OutlineElement(
label, ElementType.JAVA_BLOCK, offset, length, List.of()));
} else if (SandboxHintPartitionScanner.METADATA.equals(type)) {
String text = document.get(offset, length).trim();
elements.add(new OutlineElement(
text, ElementType.METADATA, offset, length, List.of()));
} else if (SandboxHintPartitionScanner.COMMENT.equals(type)) {
// Skip comments in outline
} else {
// Default content — split into individual rules using ';;' separators
String text = document.get(offset, length);
int start = 0;
while (start < text.length()) {
int sepIndex = text.indexOf(";;", start); //$NON-NLS-1$
int end = sepIndex == -1 ? text.length() : sepIndex;
int contentStart = start;
int contentEnd = end;
while (contentStart < contentEnd && Character.isWhitespace(text.charAt(contentStart))) {
contentStart++;
}
while (contentEnd > contentStart && Character.isWhitespace(text.charAt(contentEnd - 1))) {
contentEnd--;
}
if (contentStart < contentEnd) {
String ruleText = text.substring(contentStart, contentEnd);
if (ruleText.contains("=>")) { //$NON-NLS-1$
ruleIndex++;
String ruleLabel = buildRuleLabel(ruleText, ruleIndex);
int ruleOffsetInDocument = offset + contentStart;
int ruleLength = contentEnd - contentStart;
elements.add(new OutlineElement(
ruleLabel, ElementType.RULE, ruleOffsetInDocument, ruleLength, List.of()));
}
}
if (sepIndex == -1) {
break;
}
start = sepIndex + 2; // skip past ';;'
}
}
}
} catch (BadLocationException e) {
// ignore — return partial results
}
return elements;
}
private static String buildJavaBlockLabel(String text) {
// Extract first method signature for a more descriptive label
String stripped = text;
if (stripped.startsWith("<?")) { //$NON-NLS-1$
stripped = stripped.substring(2);
}
if (stripped.endsWith("?>")) { //$NON-NLS-1$
stripped = stripped.substring(0, stripped.length() - 2);
}
stripped = stripped.trim();
// Find "public boolean methodName" or "public void methodName"
int parenIdx = stripped.indexOf('(');
if (parenIdx > 0) {
String beforeParen = stripped.substring(0, parenIdx).trim();
int lastSpace = beforeParen.lastIndexOf(' ');
if (lastSpace > 0) {
String methodName = beforeParen.substring(lastSpace + 1);
return "<? " + methodName + "() ?>"; //$NON-NLS-1$ //$NON-NLS-2$
}
}
if (stripped.length() > 40) {
stripped = stripped.substring(0, 40) + "..."; //$NON-NLS-1$
}
return "<? " + stripped + " ?>"; //$NON-NLS-1$ //$NON-NLS-2$
}
private static String buildRuleLabel(String text, int index) {
String trimmed = text.trim();
// Extract the pattern (first line typically)
int newlineIdx = trimmed.indexOf('\n');
String firstLine = (newlineIdx > 0) ? trimmed.substring(0, newlineIdx).trim() : trimmed;
if (firstLine.length() > 60) {
firstLine = firstLine.substring(0, 60) + "..."; //$NON-NLS-1$
}
return "Rule " + index + ": " + firstLine; //$NON-NLS-1$ //$NON-NLS-2$
}
}
/**
* Label provider for outline elements.
*/
private static class OutlineLabelProvider extends LabelProvider {
@Override
public String getText(Object element) {
if (element instanceof OutlineElement outlineElement) {
return outlineElement.label();
}
return super.getText(element);
}
@Override
public Image getImage(Object element) {
// No custom images for now; could add JDT/PDE shared images later
return null;
}
}
}