SandboxHintJavaFormatter.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.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.formatter.CodeFormatter;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.text.edits.TextEdit;
/**
* Formats embedded Java code ({@code <? ?>}) blocks in {@code .sandbox-hint}
* files by delegating to JDT's {@link CodeFormatter}.
*
* <p>The formatter wraps the embedded Java source in a synthetic class body
* (matching the structure used by
* {@link org.sandbox.jdt.triggerpattern.internal.EmbeddedJavaCompiler}),
* formats it with JDT's formatter, and then extracts and applies the
* formatted body back to the document.</p>
*
* @since 1.5.0
*/
public final class SandboxHintJavaFormatter {
private static final Logger LOGGER = Logger.getLogger(SandboxHintJavaFormatter.class.getName());
private static final String SYNTHETIC_HEADER = "class _Fmt {\n"; //$NON-NLS-1$
private static final String SYNTHETIC_FOOTER = "\n}"; //$NON-NLS-1$
private SandboxHintJavaFormatter() {
// utility class
}
/**
* Formats all embedded Java ({@code <? ?>}) blocks in the given document.
*
* @param document the document to format
*/
public static void formatEmbeddedJavaBlocks(IDocument document) {
if (document == null) {
return;
}
try {
ITypedRegion[] partitions = document.computePartitioning(0, document.getLength());
// Process partitions in reverse order to preserve offsets
for (int i = partitions.length - 1; i >= 0; i--) {
ITypedRegion partition = partitions[i];
if (SandboxHintPartitionScanner.JAVA_CODE.equals(partition.getType())) {
formatJavaPartition(document, partition);
}
}
} catch (BadLocationException e) {
LOGGER.log(Level.WARNING, "Failed to format embedded Java blocks", e); //$NON-NLS-1$
}
}
/**
* Formats a single {@code <? ?>} partition.
*/
private static void formatJavaPartition(IDocument document, ITypedRegion partition)
throws BadLocationException {
int offset = partition.getOffset();
int length = partition.getLength();
String text = document.get(offset, length);
// Strip <? and ?>
String javaSource = text;
boolean hasDelimiters = false;
if (text.startsWith("<?") && text.endsWith("?>")) { //$NON-NLS-1$ //$NON-NLS-2$
javaSource = text.substring(2, text.length() - 2);
hasDelimiters = true;
}
// Wrap in synthetic class
String syntheticSource = SYNTHETIC_HEADER + javaSource + SYNTHETIC_FOOTER;
Map<String, String> options = JavaCore.getOptions();
CodeFormatter formatter = ToolFactory.createCodeFormatter(options);
TextEdit edit = formatter.format(
CodeFormatter.K_COMPILATION_UNIT,
syntheticSource,
0, syntheticSource.length(),
0, null);
if (edit == null) {
return; // formatting failed (e.g., syntax errors)
}
// Apply to a temporary document to get the formatted result
org.eclipse.jface.text.Document tempDoc = new org.eclipse.jface.text.Document(syntheticSource);
edit.apply(tempDoc);
String formatted = tempDoc.get();
// Extract the body between the synthetic header/footer
int bodyStart = formatted.indexOf('{');
int bodyEnd = formatted.lastIndexOf('}');
if (bodyStart < 0 || bodyEnd <= bodyStart) {
return;
}
String formattedBody = formatted.substring(bodyStart + 1, bodyEnd);
// Trim leading/trailing newlines from the body
if (formattedBody.startsWith("\n")) { //$NON-NLS-1$
formattedBody = formattedBody.substring(1);
}
if (formattedBody.endsWith("\n")) { //$NON-NLS-1$
formattedBody = formattedBody.substring(0, formattedBody.length() - 1);
}
// Rebuild with delimiters
String replacement;
if (hasDelimiters) {
replacement = "<?\n" + formattedBody + "\n?>"; //$NON-NLS-1$ //$NON-NLS-2$
} else {
replacement = formattedBody;
}
// Only replace if changed
if (!replacement.equals(text)) {
document.replace(offset, length, replacement);
}
}
}