SandboxHintBreakpointAdapter.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.logging.Level;
import java.util.logging.Logger;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.ILineBreakpoint;
import org.eclipse.debug.ui.actions.IToggleBreakpointsTarget;
import org.eclipse.jdt.debug.core.JDIDebugModel;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.texteditor.ITextEditor;

/**
 * Breakpoint adapter for {@code .sandbox-hint} files.
 *
 * <p>Supports toggling line breakpoints inside embedded Java ({@code <? ?>})
 * blocks. Double-clicking the editor ruler inside an embedded Java region
 * creates a Java line breakpoint on the corresponding line in the synthetic
 * generated class.</p>
 *
 * <p>The breakpoint is stored with the hint file as its resource, and the
 * synthetic class type name is associated for JDT debug matching.</p>
 *
 * @since 1.5.0
 */
public class SandboxHintBreakpointAdapter implements IToggleBreakpointsTarget {

	private static final Logger LOGGER = Logger.getLogger(SandboxHintBreakpointAdapter.class.getName());

	@Override
	public void toggleLineBreakpoints(IWorkbenchPart part, ISelection selection) throws CoreException {
		if (!(part instanceof ITextEditor textEditor)) {
			return;
		}
		if (!(selection instanceof ITextSelection textSelection)) {
			return;
		}

		IEditorInput input = textEditor.getEditorInput();
		if (!(input instanceof IFileEditorInput fileInput)) {
			return;
		}

		IFile file = fileInput.getFile();
		int lineNumber = textSelection.getStartLine() + 1; // 1-based

		// Check if the selection is inside a <? ?> Java code partition
		IDocument document = textEditor.getDocumentProvider().getDocument(input);
		if (!isInsideJavaPartition(document, textSelection.getOffset())) {
			LOGGER.log(Level.FINE, "Breakpoint toggle at line {0}: not inside Java partition", lineNumber); //$NON-NLS-1$
			return;
		}

		// Check for existing breakpoint at this line
		IBreakpoint existingBreakpoint = findBreakpoint(file, lineNumber);
		if (existingBreakpoint != null) {
			DebugPlugin.getDefault().getBreakpointManager().removeBreakpoint(existingBreakpoint, true);
			LOGGER.log(Level.FINE, "Removed breakpoint at line {0}", lineNumber); //$NON-NLS-1$
			return;
		}

		// Determine the synthetic class type name from the source locator mappings
		String typeName = findSyntheticTypeName(file, lineNumber);

		// Create a Java line breakpoint
		JDIDebugModel.createLineBreakpoint(
				file,           // resource
				typeName,       // type name
				lineNumber,     // line number
				-1,             // char start
				-1,             // char end
				0,              // hit count
				true,           // register
				null            // attributes
		);

		LOGGER.log(Level.FINE, "Created breakpoint at line {0} for type {1}", //$NON-NLS-1$
				new Object[] { lineNumber, typeName });
	}

	@Override
	public boolean canToggleLineBreakpoints(IWorkbenchPart part, ISelection selection) {
		if (!(part instanceof ITextEditor textEditor)) {
			return false;
		}
		if (!(selection instanceof ITextSelection textSelection)) {
			return false;
		}
		IDocument document = textEditor.getDocumentProvider()
				.getDocument(textEditor.getEditorInput());
		return isInsideJavaPartition(document, textSelection.getOffset());
	}

	@Override
	public void toggleMethodBreakpoints(IWorkbenchPart part, ISelection selection) throws CoreException {
		// Not supported for hint files
	}

	@Override
	public boolean canToggleMethodBreakpoints(IWorkbenchPart part, ISelection selection) {
		return false;
	}

	@Override
	public void toggleWatchpoints(IWorkbenchPart part, ISelection selection) throws CoreException {
		// Not supported for hint files
	}

	@Override
	public boolean canToggleWatchpoints(IWorkbenchPart part, ISelection selection) {
		return false;
	}

	/**
	 * Checks if the given offset is inside a {@link SandboxHintPartitionScanner#JAVA_CODE} partition.
	 */
	private boolean isInsideJavaPartition(IDocument document, int offset) {
		if (document == null) {
			return false;
		}
		try {
			ITypedRegion partition = document.getPartition(offset);
			return SandboxHintPartitionScanner.JAVA_CODE.equals(partition.getType());
		} catch (BadLocationException e) {
			return false;
		}
	}

	/**
	 * Finds an existing breakpoint at the given line in the file.
	 */
	private IBreakpoint findBreakpoint(IResource resource, int lineNumber) {
		IBreakpoint[] breakpoints = DebugPlugin.getDefault().getBreakpointManager()
				.getBreakpoints(JDIDebugModel.getPluginIdentifier());
		for (IBreakpoint bp : breakpoints) {
			if (resource.equals(bp.getMarker().getResource()) && bp instanceof ILineBreakpoint lineBp) {
				try {
					if (lineBp.getLineNumber() == lineNumber) {
						return bp;
					}
				} catch (CoreException e) {
					// ignore
				}
			}
		}
		return null;
	}

	/**
	 * Attempts to find the synthetic type name for a breakpoint at the given line.
	 * Looks up registered source mappings from the {@link EmbeddedJavaSourceLocator}.
	 * Falls back to a stable name derived from the hint filename when no active
	 * mapping is available yet.
	 */
	private String findSyntheticTypeName(IFile file, int lineNumber) {
		if (file == null) {
			return null;
		}
		// When EmbeddedJavaSourceLocator has active mappings, query them to
		// find the exact synthetic class name and mapped line.
		// For now, derive a stable name from the hint file name.
		String baseName = sanitize(file.getName());
		if (baseName.isEmpty()) {
			baseName = "unknown"; //$NON-NLS-1$
		}
		return EmbeddedJavaSourceLocator.SYNTHETIC_PREFIX + baseName;
	}

	/**
	 * Sanitizes a filename for use as a Java identifier suffix.
	 */
	private String sanitize(String name) {
		if (name == null) {
			return "unknown"; //$NON-NLS-1$
		}
		// Remove file extension
		int dotIdx = name.lastIndexOf('.');
		if (dotIdx > 0) {
			name = name.substring(0, dotIdx);
		}
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < name.length(); i++) {
			char c = name.charAt(i);
			if (Character.isJavaIdentifierPart(c)) {
				sb.append(c);
			} else {
				sb.append('_');
			}
		}
		return sb.toString();
	}
}