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

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.ui.text.java.CompletionProposalCollector;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.PlatformUI;

/**
 * Content assist processor for embedded Java code ({@code <? ?>}) regions
 * in {@code .sandbox-hint} files.
 *
 * <p>Delegates to JDT's {@link CompletionProposalCollector} by creating a
 * synthetic {@link ICompilationUnit} working copy from the embedded Java source.
 * This provides full context-aware Java completions including all keywords,
 * types from the project classpath, methods, fields, and local variables.</p>
 *
 * <p>The synthetic compilation unit wraps the embedded code in a class body,
 * matching the structure used by {@link org.sandbox.jdt.triggerpattern.internal.EmbeddedJavaCompiler}.</p>
 *
 * @since 1.5.0
 */
public class EmbeddedJavaContentAssistProcessor implements IContentAssistProcessor {

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

	private static final String SYNTHETIC_PACKAGE = "org.sandbox.generated"; //$NON-NLS-1$
	private static final String SYNTHETIC_CLASS_NAME = "HintCode_assist"; //$NON-NLS-1$

	/**
	 * Header prepended to the embedded Java source to form a valid compilation unit.
	 * The offset into this header is used to map completion positions.
	 */
	private static final String SYNTHETIC_HEADER =
			"package " + SYNTHETIC_PACKAGE + ";\n" + //$NON-NLS-1$ //$NON-NLS-2$
			"import org.eclipse.jdt.core.dom.*;\n" + //$NON-NLS-1$
			"public class " + SYNTHETIC_CLASS_NAME + " {\n"; //$NON-NLS-1$ //$NON-NLS-2$

	private static final String SYNTHETIC_FOOTER = "\n}\n"; //$NON-NLS-1$

	@Override
	public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
		IDocument document = viewer.getDocument();

		// Extract the embedded Java source from the <? ?> partition
		String javaSource = extractEmbeddedJavaSource(document, offset);
		if (javaSource == null) {
			return new ICompletionProposal[0];
		}

		// Calculate offset within the embedded Java source
		int partitionStart = getPartitionStart(document, offset);
		int offsetInEmbedded = offset - partitionStart;

		// Build synthetic compilation unit source
		String syntheticSource = SYNTHETIC_HEADER + javaSource + SYNTHETIC_FOOTER;
		int syntheticOffset = SYNTHETIC_HEADER.length() + offsetInEmbedded;

		// Try to get an IJavaProject from the active editor
		IJavaProject javaProject = getJavaProject();
		if (javaProject == null) {
			LOGGER.log(Level.FINE, "No IJavaProject available, cannot provide JDT content assist"); //$NON-NLS-1$
			return new ICompletionProposal[0];
		}

		return computeJdtProposals(javaProject, syntheticSource, syntheticOffset,
				offset, offsetInEmbedded);
	}

	/**
	 * Delegates to JDT's code completion engine via a synthetic working copy.
	 */
	private ICompletionProposal[] computeJdtProposals(IJavaProject javaProject,
			String syntheticSource, int syntheticOffset, int documentOffset, int offsetInEmbedded) {
		ICompilationUnit workingCopy = null;
		try {
			// Find or create a source folder for the synthetic unit
			IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
			IPackageFragmentRoot sourceRoot = null;
			for (IPackageFragmentRoot root : roots) {
				if (root.getKind() == IPackageFragmentRoot.K_SOURCE) {
					sourceRoot = root;
					break;
				}
			}
			if (sourceRoot == null) {
				LOGGER.log(Level.FINE, "No source root found in project"); //$NON-NLS-1$
				return new ICompletionProposal[0];
			}

			IPackageFragment pkg = sourceRoot.getPackageFragment(SYNTHETIC_PACKAGE);
			ICompilationUnit originalUnit = pkg.getCompilationUnit(SYNTHETIC_CLASS_NAME + ".java"); //$NON-NLS-1$
			workingCopy = originalUnit.getWorkingCopy(null);
			workingCopy.getBuffer().setContents(syntheticSource);

			// Collect proposals via JDT's CompletionProposalCollector
			CompletionProposalCollector collector = new CompletionProposalCollector(workingCopy);
			collector.setReplacementLength(0);

			workingCopy.codeComplete(syntheticOffset, collector);

			ICompletionProposal[] jdtProposals = collector.getJavaCompletionProposals();

			// Remap proposal offsets from synthetic source back to the hint document
			int offsetDelta = documentOffset - syntheticOffset;
			List<ICompletionProposal> remapped = new ArrayList<>(jdtProposals.length);
			for (ICompletionProposal proposal : jdtProposals) {
				remapped.add(new OffsetRemappingProposal(proposal, offsetDelta));
			}
			return remapped.toArray(new ICompletionProposal[0]);

		} catch (JavaModelException e) {
			LOGGER.log(Level.WARNING, "JDT code completion failed", e); //$NON-NLS-1$
			return new ICompletionProposal[0];
		} finally {
			if (workingCopy != null) {
				try {
					workingCopy.discardWorkingCopy();
				} catch (JavaModelException e) {
					LOGGER.log(Level.FINE, "Failed to discard working copy", e); //$NON-NLS-1$
				}
			}
		}
	}

	@Override
	public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
		return new IContextInformation[0];
	}

	@Override
	public char[] getCompletionProposalAutoActivationCharacters() {
		return new char[] { '.' };
	}

	@Override
	public char[] getContextInformationAutoActivationCharacters() {
		return null;
	}

	@Override
	public String getErrorMessage() {
		return null;
	}

	@Override
	public IContextInformationValidator getContextInformationValidator() {
		return null;
	}

	/**
	 * Extracts the full embedded Java source from the {@code <? ?>} partition
	 * containing the given offset.
	 */
	private String extractEmbeddedJavaSource(IDocument document, int offset) {
		try {
			ITypedRegion partition = document.getPartition(offset);
			if (!SandboxHintPartitionScanner.JAVA_CODE.equals(partition.getType())) {
				return null;
			}
			String partitionText = document.get(partition.getOffset(), partition.getLength());
			// Strip the <? and ?> delimiters
			if (partitionText.startsWith("<?") && partitionText.endsWith("?>")) { //$NON-NLS-1$ //$NON-NLS-2$
				return partitionText.substring(2, partitionText.length() - 2);
			}
			return partitionText;
		} catch (BadLocationException e) {
			return null;
		}
	}

	/**
	 * Returns the start offset of the embedded Java content (after the {@code <?} delimiter).
	 */
	private int getPartitionStart(IDocument document, int offset) {
		try {
			ITypedRegion partition = document.getPartition(offset);
			String partitionText = document.get(partition.getOffset(), partition.getLength());
			// Account for the <? delimiter
			int delimiterLength = partitionText.startsWith("<?") ? 2 : 0; //$NON-NLS-1$
			return partition.getOffset() + delimiterLength;
		} catch (BadLocationException e) {
			return offset;
		}
	}

	/**
	 * Attempts to obtain the {@link IJavaProject} from the active editor.
	 */
	private IJavaProject getJavaProject() {
		try {
			IEditorPart editor = PlatformUI.getWorkbench()
					.getActiveWorkbenchWindow()
					.getActivePage()
					.getActiveEditor();
			if (editor == null) {
				return null;
			}
			IEditorInput input = editor.getEditorInput();
			if (input instanceof IFileEditorInput fileInput) {
				IFile file = fileInput.getFile();
				IProject project = file.getProject();
				if (project.hasNature(JavaCore.NATURE_ID)) {
					return JavaCore.create(project);
				}
			}
		} catch (Exception e) {
			LOGGER.log(Level.FINE, "Could not determine Java project", e); //$NON-NLS-1$
		}
		return null;
	}
}