SandboxHintReconcilingStrategy.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.List;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.texteditor.ITextEditor;
import org.sandbox.jdt.triggerpattern.api.EmbeddedJavaBlock;
import org.sandbox.jdt.triggerpattern.api.HintFile;
import org.sandbox.jdt.triggerpattern.internal.EmbeddedJavaCompiler;
import org.sandbox.jdt.triggerpattern.internal.EmbeddedJavaCompiler.CompilationResult;
import org.sandbox.jdt.triggerpattern.internal.HintFileParser;
import org.sandbox.jdt.triggerpattern.internal.HintFileParser.HintParseException;

/**
 * Reconciling strategy for {@code .sandbox-hint} files.
 *
 * <p>Validates the document content using {@link HintFileParser} and creates
 * error markers for parse errors. Also compiles embedded Java ({@code <? ?>})
 * blocks via {@link EmbeddedJavaCompiler} and maps compilation errors back to
 * the hint file line numbers.</p>
 *
 * @since 1.3.6
 */
public class SandboxHintReconcilingStrategy implements IReconcilingStrategy {

	/**
	 * Marker type for hint file parse errors.
	 */
	private static final String MARKER_TYPE = "org.eclipse.core.resources.problemmarker"; //$NON-NLS-1$

	/**
	 * Marker type for embedded Java compilation errors.
	 *
	 * @since 1.5.0
	 */
	private static final String EMBEDDED_JAVA_MARKER_TYPE = "sandbox_common.org.sandbox.jdt.triggerpattern.embeddedJavaProblem"; //$NON-NLS-1$

	private IDocument document;
	private ISourceViewer sourceViewer;
	private SandboxHintEditor editor;

	/**
	 * Sets the source viewer for accessing the editor input.
	 *
	 * @param viewer the source viewer
	 */
	public void setSourceViewer(ISourceViewer viewer) {
		this.sourceViewer = viewer;
	}

	/**
	 * Sets the editor for triggering folding and outline updates after reconciliation.
	 *
	 * @param editor the hint editor
	 * @since 1.5.0
	 */
	public void setEditor(SandboxHintEditor editor) {
		this.editor = editor;
	}

	@Override
	public void setDocument(IDocument document) {
		this.document = document;
	}

	@Override
	public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) {
		reconcile(subRegion);
	}

	@Override
	public void reconcile(IRegion partition) {
		if (document == null) {
			return;
		}

		IFile file = getFile();
		if (file == null || !file.exists()) {
			return;
		}

		// Clear old markers
		try {
			file.deleteMarkers(MARKER_TYPE, true, IResource.DEPTH_ZERO);
			file.deleteMarkers(EMBEDDED_JAVA_MARKER_TYPE, true, IResource.DEPTH_ZERO);
		} catch (CoreException e) {
			logError("Failed to clear markers", e); //$NON-NLS-1$
		}

		// Validate DSL
		String content = document.get();
		HintFileParser parser = new HintFileParser();
		HintFile hintFile = null;
		try {
			hintFile = parser.parse(content);
		} catch (HintParseException e) {
			createErrorMarker(file, e);
		}

		// Validate embedded Java blocks
		if (hintFile != null) {
			validateEmbeddedJavaBlocks(file, hintFile);
		}

		// Update folding and outline after reconciliation
		if (editor != null) {
			Display display = Display.getDefault();
			if (display != null && !display.isDisposed()) {
				display.asyncExec(() -> {
					editor.updateFolding();
					editor.updateOutline();
				});
			}
		}
	}

	/**
	 * Compiles each embedded Java block and creates markers for compilation errors.
	 *
	 * @since 1.5.0
	 */
	private void validateEmbeddedJavaBlocks(IFile file, HintFile hintFile) {
		List<EmbeddedJavaBlock> blocks = hintFile.getEmbeddedJavaBlocks();
		String ruleId = hintFile.getId();

		for (EmbeddedJavaBlock block : blocks) {
			if (block.getSource().isBlank()) {
				continue;
			}
			CompilationResult result = EmbeddedJavaCompiler.compile(block, ruleId);
			if (result.hasErrors()) {
				createEmbeddedJavaMarkers(file, block, result);
			}
		}
	}

	/**
	 * Creates markers for compilation problems in an embedded Java block.
	 * Maps synthetic class line numbers back to hint file line numbers and
	 * adjusts character positions by subtracting the synthetic header length.
	 */
	private void createEmbeddedJavaMarkers(IFile file, EmbeddedJavaBlock block,
			CompilationResult result) {
		for (IProblem problem : result.problems()) {
			if (!problem.isError()) {
				continue;
			}
			try {
				IMarker marker = file.createMarker(EMBEDDED_JAVA_MARKER_TYPE);
				marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
				marker.setAttribute(IMarker.MESSAGE, problem.getMessage());

				// Map synthetic class line back to hint file line
				int syntheticLine = problem.getSourceLineNumber();
				int hintLine = syntheticLine + result.lineOffset();
				if (hintLine > 0) {
					marker.setAttribute(IMarker.LINE_NUMBER, hintLine);
				}

				// Map character positions: problem offsets are in the synthetic
				// source which has a header before the embedded code. Subtract
				// the header length and add the block's start offset plus the
				// '<?' delimiter length to map back to the hint document.
				int sourceStart = problem.getSourceStart();
				int sourceEnd = problem.getSourceEnd();
				if (sourceStart >= 0 && sourceEnd >= 0) {
					int headerLength = result.syntheticHeaderLength();
					int delimiterLength = 2; // length of '<?' delimiter
					int hintStart = block.getStartOffset() + delimiterLength + (sourceStart - headerLength);
					int hintEnd = block.getStartOffset() + delimiterLength + (sourceEnd - headerLength) + 1;
					if (hintStart >= 0 && hintEnd > hintStart) {
						marker.setAttribute(IMarker.CHAR_START, hintStart);
						marker.setAttribute(IMarker.CHAR_END, hintEnd);
					}
				}
			} catch (CoreException e) {
				logError("Failed to create embedded Java marker", e); //$NON-NLS-1$
			}
		}
	}

	/**
	 * Creates an error marker for a parse exception.
	 */
	private void createErrorMarker(IFile file, HintParseException e) {
		try {
			IMarker marker = file.createMarker(MARKER_TYPE);
			marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
			marker.setAttribute(IMarker.MESSAGE, e.getMessage());
			int lineNumber = extractLineNumber(e.getMessage());
			if (lineNumber > 0) {
				marker.setAttribute(IMarker.LINE_NUMBER, lineNumber);
			}
		} catch (CoreException ce) {
			logError("Failed to create marker", ce); //$NON-NLS-1$
		}
	}

	/**
	 * Extracts a line number from a parse error message.
	 *
	 * <p>The {@link HintFileParser} includes line numbers in its error messages
	 * in the format "Line N: ...".</p>
	 */
	private int extractLineNumber(String message) {
		if (message != null && message.startsWith("Line ")) { //$NON-NLS-1$
			int colonIdx = message.indexOf(':');
			if (colonIdx > 5) {
				try {
					return Integer.parseInt(message.substring(5, colonIdx).trim());
				} catch (NumberFormatException e) {
					// ignore
				}
			}
		}
		return -1;
	}

	/**
	 * Gets the {@link IFile} associated with the current editor.
	 */
	private IFile getFile() {
		if (sourceViewer == null) {
			return null;
		}
		Object adapter = sourceViewer.getTextWidget().getData("org.eclipse.ui.texteditor"); //$NON-NLS-1$
		if (adapter instanceof ITextEditor textEditor) {
			IEditorInput input = textEditor.getEditorInput();
			if (input instanceof IFileEditorInput fileInput) {
				return fileInput.getFile();
			}
		}
		return null;
	}

	private void logError(String message, CoreException e) {
		ILog log = Platform.getLog(SandboxHintReconcilingStrategy.class);
		log.error(message, e);
	}
}