TriggerPatternQuickAssistProcessor.java

/*******************************************************************************
 * Copyright (c) 2025 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 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.core.dom.rewrite.ASTRewrite;
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.sandbox.jdt.triggerpattern.api.HintContext;
import org.sandbox.jdt.triggerpattern.api.Match;
import org.sandbox.jdt.triggerpattern.api.TriggerPatternEngine;
import org.sandbox.jdt.triggerpattern.internal.HintRegistry;
import org.sandbox.jdt.triggerpattern.internal.HintRegistry.HintDescriptor;

/**
 * Quick Assist processor for trigger pattern hints.
 * 
 * <p>This processor finds matching patterns at the cursor location and invokes
 * registered hint methods to provide completion proposals.</p>
 * 
 * @since 1.2.2
 */
public class TriggerPatternQuickAssistProcessor implements IQuickAssistProcessor {
	
	private static final HintRegistry REGISTRY = new HintRegistry();
	private final TriggerPatternEngine engine = new TriggerPatternEngine();
	
	@Override
	public boolean hasAssists(IInvocationContext context) throws CoreException {
		// Quick check - we could optimize this by caching patterns
		return !REGISTRY.getHints().isEmpty();
	}
	
	@Override
	public IJavaCompletionProposal[] getAssists(IInvocationContext context, IProblemLocation[] locations)
			throws CoreException {
		
		ICompilationUnit icu = context.getCompilationUnit();
		if (icu == null) {
			return null;
		}
		
		// Parse the compilation unit
		CompilationUnit cu = getCompilationUnit(icu);
		if (cu == null) {
			return null;
		}
		
		// Get the AST node at the cursor position
		int offset = context.getSelectionOffset();
		ASTNode coveringNode = getCoveringNode(cu, offset);
		if (coveringNode == null) {
			return null;
		}
		
		List<IJavaCompletionProposal> proposals = new ArrayList<>();
		
		// Check each registered hint
		for (HintDescriptor hint : REGISTRY.getHints()) {
			if (!hint.isEnabledByDefault()) {
				continue;
			}
			
			// Find matches for this pattern near the cursor
			List<Match> matches = engine.findMatches(cu, hint.getPattern());
			
			// Check if any match contains the cursor position
			for (Match match : matches) {
				if (containsOffset(match, offset)) {
					// Create hint context
					ASTRewrite rewrite = ASTRewrite.create(cu.getAST());
					HintContext hintContext = new HintContext(cu, icu, match, rewrite);
					
					try {
						// Invoke the hint method
						Object result = hint.invoke(hintContext);
						
						// Convert result to proposals
						if (result instanceof IJavaCompletionProposal) {
							proposals.add((IJavaCompletionProposal) result);
						} else if (result instanceof List) {
							@SuppressWarnings("unchecked")
							List<IJavaCompletionProposal> list = (List<IJavaCompletionProposal>) result;
							proposals.addAll(list);
						}
					} catch (Exception e) {
						// Log error but continue with other hints
						ILog log = Platform.getLog(TriggerPatternQuickAssistProcessor.class);
						log.log(Status.error("Error invoking hint method", 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);
	}
	
	/**
	 * Gets the AST node covering the given offset.
	 */
	private ASTNode getCoveringNode(CompilationUnit cu, int offset) {
		// Find the smallest node that covers the offset
		class NodeFinder extends org.eclipse.jdt.core.dom.ASTVisitor {
			ASTNode result = null;
			int targetOffset;
			
			NodeFinder(int offset) {
				this.targetOffset = offset;
			}
			
			@Override
			public void preVisit(ASTNode node) {
				int start = node.getStartPosition();
				int end = start + node.getLength();
				
				if (start <= targetOffset && targetOffset <= end) {
					// This node covers the offset
					if (result == null || node.getLength() < result.getLength()) {
						result = node;
					}
				}
			}
		}
		
		NodeFinder finder = new NodeFinder(offset);
		cu.accept(finder);
		return finder.result;
	}
	
	/**
	 * Checks if a match contains the given offset.
	 */
	private boolean containsOffset(Match match, int offset) {
		int start = match.getOffset();
		int end = start + match.getLength();
		return start <= offset && offset <= end;
	}
}