HintFileQuickAssistProcessor.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.ui;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.ui.text.java.IInvocationContext;
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
import org.eclipse.jdt.ui.text.java.IProblemLocation;
import org.eclipse.jdt.ui.text.java.IQuickAssistProcessor;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.sandbox.jdt.triggerpattern.api.BatchTransformationProcessor;
import org.sandbox.jdt.triggerpattern.api.BatchTransformationProcessor.TransformationResult;
import org.sandbox.jdt.triggerpattern.api.HintFile;
import org.sandbox.jdt.triggerpattern.api.TransformationRule;
import org.sandbox.jdt.triggerpattern.internal.HintFileRegistry;

/**
 * Quick Assist processor that creates proposals from {@code .sandbox-hint} files.
 *
 * <p>This processor finds matching transformation rules from registered
 * {@code .sandbox-hint} files at the cursor location and creates
 * completion proposals for each match that has a replacement.</p>
 *
 * <p>This is complementary to {@link TriggerPatternQuickAssistProcessor},
 * which handles annotation-based {@code @TriggerPattern} hints.
 * This processor handles DSL-based {@code .sandbox-hint} file rules.</p>
 *
 * @since 1.3.5
 */
public class HintFileQuickAssistProcessor implements IQuickAssistProcessor {

	@Override
	public boolean hasAssists(IInvocationContext context) throws CoreException {
		HintFileRegistry registry = HintFileRegistry.getInstance();
		// Ensure bundled libraries are loaded so assists are discoverable in a fresh session
		registry.loadBundledLibraries(HintFileQuickAssistProcessor.class.getClassLoader());
		return !registry.getAllHintFiles().isEmpty();
	}

	@Override
	public IJavaCompletionProposal[] getAssists(IInvocationContext context, IProblemLocation[] locations)
			throws CoreException {

		ICompilationUnit icu = context.getCompilationUnit();
		if (icu == null) {
			return null;
		}

		CompilationUnit cu = getCompilationUnit(icu);
		if (cu == null) {
			return null;
		}

		int offset = context.getSelectionOffset();
		List<IJavaCompletionProposal> proposals = new ArrayList<>();

		HintFileRegistry registry = HintFileRegistry.getInstance();
		// Ensure bundled libraries are loaded
		registry.loadBundledLibraries(HintFileQuickAssistProcessor.class.getClassLoader());

		// Load project-level .sandbox-hint files if available
		if (icu.getJavaProject() != null) {
			org.eclipse.core.resources.IProject project = icu.getJavaProject().getProject();
			registry.loadProjectHintFiles(project);
		}

		for (Map.Entry<String, HintFile> entry : registry.getAllHintFiles().entrySet()) {
			HintFile hintFile = entry.getValue();

			try {
				List<TransformationRule> resolvedRules = registry.resolveIncludes(hintFile);
				BatchTransformationProcessor processor = new BatchTransformationProcessor(hintFile, resolvedRules);
				List<TransformationResult> results = processor.process(cu);

				for (TransformationResult result : results) {
					if (result.hasReplacement() && containsOffset(result, offset)) {
						proposals.add(new HintFileProposal(result, cu));
					}
				}
			} catch (Exception e) {
				ILog log = Platform.getLog(HintFileQuickAssistProcessor.class);
				log.log(Status.error("Error processing hint file: " + entry.getKey(), e)); //$NON-NLS-1$
			}
		}

		return proposals.isEmpty() ? null : proposals.toArray(new IJavaCompletionProposal[0]);
	}

	/**
	 * Parses the compilation unit.
	 */
	private CompilationUnit getCompilationUnit(ICompilationUnit icu) {
		ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
		parser.setSource(icu);
		parser.setResolveBindings(false);
		return (CompilationUnit) parser.createAST(null);
	}

	/**
	 * Checks if a transformation result contains the given offset.
	 */
	private boolean containsOffset(TransformationResult result, int offset) {
		int start = result.match().getOffset();
		int end = start + result.match().getLength();
		return start <= offset && offset <= end;
	}

	/**
	 * Quick Assist proposal generated from a {@code .sandbox-hint} file rule match.
	 */
	private static class HintFileProposal implements IJavaCompletionProposal {

		private final TransformationResult result;
		private final CompilationUnit cu;

		HintFileProposal(TransformationResult result, CompilationUnit cu) {
			this.result = result;
			this.cu = cu;
		}

		@Override
		public void apply(IDocument document) {
			try {
				ASTNode matchedNode = result.match().getMatchedNode();
				String replacement = result.replacement();
				if (matchedNode != null && replacement != null) {
					int start = matchedNode.getStartPosition();
					int length = matchedNode.getLength();
					document.replace(start, length, replacement);
				}
			} catch (Exception e) {
				ILog log = Platform.getLog(HintFileQuickAssistProcessor.class);
				log.log(Status.error("Error applying hint file proposal", e)); //$NON-NLS-1$
			}
		}

		@Override
		public String getDisplayString() {
			String description = result.description();
			if (description != null && !description.isEmpty()) {
				return description;
			}
			return "Apply hint: " + result.matchedText() + " → " + result.replacement(); //$NON-NLS-1$ //$NON-NLS-2$
		}

		@Override
		public Point getSelection(IDocument document) {
			return null;
		}

		@Override
		public String getAdditionalProposalInfo() {
			StringBuilder sb = new StringBuilder();
			sb.append("Before: ").append(result.matchedText()).append('\n'); //$NON-NLS-1$
			sb.append("After:  ").append(result.replacement()); //$NON-NLS-1$
			return sb.toString();
		}

		@Override
		public Image getImage() {
			return null;
		}

		@Override
		public IContextInformation getContextInformation() {
			return null;
		}

		@Override
		public int getRelevance() {
			return 5;
		}
	}
}