DslFromSelectionAssistProcessor.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
 *******************************************************************************/
package org.sandbox.jdt.internal.ui.quickassist;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
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.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.sandbox.jdt.triggerpattern.llm.AiRuleInferenceEngine;
import org.sandbox.jdt.triggerpattern.llm.CommitEvaluation;
import org.sandbox.jdt.triggerpattern.mining.llm.EclipseLlmService;

/**
 * Quick assist processor that generates a TriggerPattern DSL rule from the
 * currently selected Java code using AI-powered inference.
 *
 * <p>When invoked on a text selection, it wraps the selected code as a
 * pseudo-diff and sends it to the LLM in a background {@link Job}. The
 * resulting DSL rule is opened as a new {@code .sandbox-hint} file.</p>
 *
 * @since 1.2.6
 */
public class DslFromSelectionAssistProcessor implements IQuickAssistProcessor {

	private static final ILog LOG = Platform.getLog(DslFromSelectionAssistProcessor.class);
	private static final String PROPOSAL_LABEL = "Generate DSL rule from selection (AI)"; //$NON-NLS-1$

	@Override
	public boolean hasAssists(IInvocationContext context) {
		return EclipseLlmService.getInstance().isAvailable()
				&& context.getSelectionLength() > 0;
	}

	@Override
	public IJavaCompletionProposal[] getAssists(IInvocationContext context,
			IProblemLocation[] locations) {
		if (!hasAssists(context)) {
			return new IJavaCompletionProposal[0];
		}
		int offset = context.getSelectionOffset();
		int length = context.getSelectionLength();
		return new IJavaCompletionProposal[] { new DslRuleProposal(offset, length) };
	}

	/**
	 * Completion proposal that infers a DSL rule from the selected code.
	 * The LLM call runs in a background {@link Job} to avoid blocking the UI.
	 */
	private static class DslRuleProposal implements IJavaCompletionProposal {

		private final int selectionOffset;
		private final int selectionLength;

		DslRuleProposal(int offset, int length) {
			this.selectionOffset = offset;
			this.selectionLength = length;
		}

		@Override
		public void apply(IDocument document) {
			try {
				String selectedCode = document.get(selectionOffset, selectionLength);
				Job job = new Job("Generating DSL rule from selection") { //$NON-NLS-1$
					@Override
					protected IStatus run(IProgressMonitor monitor) {
						AiRuleInferenceEngine engine = EclipseLlmService.getInstance().getEngine();
						// Wrap snippet as a pseudo-diff so the LLM can infer a generalized match rule
						String[] lines = selectedCode.split("\n", -1); //$NON-NLS-1$
						StringBuilder sb = new StringBuilder();
						sb.append("--- a/snippet.java\n+++ b/snippet.java\n"); //$NON-NLS-1$
						sb.append("@@ -1,0 +1,").append(lines.length).append(" @@\n"); //$NON-NLS-1$ //$NON-NLS-2$
						for (String line : lines) {
							sb.append('+').append(line).append('\n');
						}
						String pseudoDiff = sb.toString();
						Optional<CommitEvaluation> result = engine.inferRuleFromDiff(pseudoDiff);
						if (result.isPresent() && result.get().dslRule() != null
								&& !result.get().dslRule().isBlank()) {
							openHintFileOnUi(result.get().dslRule());
						}
						return Status.OK_STATUS;
					}
				};
				job.setUser(true);
				job.schedule();
			} catch (Exception e) {
				LOG.error("Failed to infer DSL rule from selection", e); //$NON-NLS-1$
			}
		}

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

		@Override
		public String getAdditionalProposalInfo() {
			return "Uses the configured LLM to infer a TriggerPattern DSL rule from the selected code snippet."; //$NON-NLS-1$
		}

		@Override
		public String getDisplayString() {
			return PROPOSAL_LABEL;
		}

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

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

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

		private static void openHintFileOnUi(String ruleContent) {
			Display.getDefault().asyncExec(() -> {
				try {
					IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
					if (projects.length == 0) {
						return;
					}
					IProject project = projects[0];
					String fileName = "inferred-rule-" + System.currentTimeMillis() + ".sandbox-hint"; //$NON-NLS-1$ //$NON-NLS-2$
					IFile file = project.getFile(new Path(fileName));
					file.create(
							new ByteArrayInputStream(ruleContent.getBytes(StandardCharsets.UTF_8)),
							true, null);
					IWorkbenchPage page = PlatformUI.getWorkbench()
							.getActiveWorkbenchWindow().getActivePage();
					if (page != null) {
						IDE.openEditor(page, file);
					}
				} catch (Exception e) {
					LOG.error("Failed to open hint file for inferred rule", e); //$NON-NLS-1$
				}
			});
		}
	}
}