RefactoringMiningView.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.views.mining;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Table;
import org.eclipse.ui.part.ViewPart;
import org.sandbox.jdt.triggerpattern.mining.analysis.CommitInfo;
import org.sandbox.jdt.triggerpattern.mining.git.CommandLineGitProvider;
import org.sandbox.jdt.triggerpattern.mining.git.GitHistoryProvider;
import org.sandbox.jdt.triggerpattern.mining.git.JGitHistoryProvider;
import org.sandbox.jdt.triggerpattern.mining.llm.EclipseLlmService;

/**
 * Eclipse view that displays Git commit history and allows asynchronous
 * AI-powered analysis of commits to infer transformation rules.
 *
 * <p>Layout:</p>
 * <ul>
 *   <li>Top: Commit table with columns (Commit, Message, Files, AI Status)</li>
 *   <li>Bottom: Detail panel showing inferred rules for the selected commit</li>
 * </ul>
 *
 * <p>The view uses {@link CommitAnalysisScheduler} to analyze commits
 * asynchronously via {@link EclipseLlmService} and updates the table
 * via {@code Display.asyncExec()}.</p>
 *
 * @since 1.2.6
 */
public class RefactoringMiningView extends ViewPart {

	/** The unique view ID registered in plugin.xml. */
	public static final String VIEW_ID = "org.sandbox.jdt.views.refactoringMining"; //$NON-NLS-1$

	private static final int DEFAULT_MAX_COMMITS = 50;

	private TableViewer commitTable;
	private InferredRuleDetailPanel detailPanel;
	private CommitAnalysisScheduler scheduler;

	private GitHistoryProvider gitProvider;
	private int maxCommits = DEFAULT_MAX_COMMITS;

	@Override
	public void createPartControl(Composite parent) {
		parent.setLayout(new GridLayout(1, false));

		gitProvider = createGitProvider();

		SashForm sash = new SashForm(parent, SWT.VERTICAL);
		sash.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

		createCommitTable(sash);
		detailPanel = new InferredRuleDetailPanel(sash);

		sash.setWeights(60, 40);

		createToolbar();
	}

	@Override
	public void setFocus() {
		if (commitTable != null && !commitTable.getTable().isDisposed()) {
			commitTable.getTable().setFocus();
		}
	}

	@Override
	public void dispose() {
		if (scheduler != null) {
			scheduler.cancelAnalysis();
		}
		super.dispose();
	}

	// ---- table creation ----

	private void createCommitTable(Composite parent) {
		commitTable = new TableViewer(parent, SWT.FULL_SELECTION | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
		Table table = commitTable.getTable();
		table.setHeaderVisible(true);
		table.setLinesVisible(true);

		createColumn("Commit", 80); //$NON-NLS-1$
		createColumn("Message", 300); //$NON-NLS-1$
		createColumn("Files", 50); //$NON-NLS-1$
		createColumn("AI Status", 80); //$NON-NLS-1$

		commitTable.setContentProvider(ArrayContentProvider.getInstance());
		commitTable.setLabelProvider(new CommitTableLabelProvider());

		commitTable.addSelectionChangedListener(new ISelectionChangedListener() {
			@Override
			public void selectionChanged(SelectionChangedEvent event) {
				IStructuredSelection sel = (IStructuredSelection) event.getSelection();
				if (sel.isEmpty()) {
					detailPanel.showRules(null);
				} else if (sel.getFirstElement() instanceof CommitTableEntry entry) {
					detailPanel.showRules(entry);
				}
			}
		});
	}

	private void createColumn(String title, int width) {
		TableViewerColumn col = new TableViewerColumn(commitTable, SWT.NONE);
		col.getColumn().setText(title);
		col.getColumn().setWidth(width);
		col.getColumn().setResizable(true);
	}

	// ---- toolbar ----

	private void createToolbar() {
		IToolBarManager mgr = getViewSite().getActionBars().getToolBarManager();

		mgr.add(new Action("Analyze Project") { //$NON-NLS-1$
			@Override
			public void run() {
				analyzeFirstGitProject();
			}
		});

		mgr.add(new Action("Stop Analysis") { //$NON-NLS-1$
			@Override
			public void run() {
				if (scheduler != null) {
					scheduler.cancelAnalysis();
				}
			}
		});

		mgr.add(new Separator());

		mgr.add(new Action("Export as .sandbox-hint") { //$NON-NLS-1$
			@Override
			public void run() {
				exportAsHintFile();
			}
		});

		mgr.update(true);
	}

	// ---- analysis ----

	/**
	 * Finds the first Git-enabled project in the workspace and starts analysis.
	 */
	void analyzeFirstGitProject() {
		IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
		for (IProject project : projects) {
			if (project.isOpen() && project.getLocation() != null) {
				Path repoPath = project.getLocation().toFile().toPath();
				analyzeRepository(repoPath);
				return;
			}
		}
	}

	/**
	 * Analyzes the given Git repository.
	 *
	 * @param repositoryPath path to the repository working directory
	 */
	void analyzeRepository(Path repositoryPath) {
		if (scheduler != null) {
			scheduler.cancelAnalysis();
		}

		try {
			List<CommitInfo> commits = gitProvider.getHistory(repositoryPath, maxCommits);
			List<CommitTableEntry> entries = new ArrayList<>();
			for (CommitInfo commit : commits) {
				entries.add(new CommitTableEntry(commit));
			}

			CommitTableEntry[] entryArray = entries.toArray(CommitTableEntry[]::new);
			commitTable.setInput(entryArray);

			scheduler = new CommitAnalysisScheduler(
					gitProvider, repositoryPath, commitTable);
			scheduler.startAnalysis(entries);
		} catch (Exception e) {
			setContentDescription("Error: " + e.getMessage()); //$NON-NLS-1$
		}
	}

	// ---- export ----

	private void exportAsHintFile() {
		List<String> allDslRules = collectSelectedDslRules();
		if (allDslRules.isEmpty()) {
			return;
		}

		FileDialog dialog = new FileDialog(getSite().getShell(), SWT.SAVE);
		dialog.setFilterExtensions(new String[] { "*.sandbox-hint" }); //$NON-NLS-1$
		dialog.setFilterNames(new String[] { "Sandbox Hint Files (*.sandbox-hint)" }); //$NON-NLS-1$
		dialog.setFileName("inferred.sandbox-hint"); //$NON-NLS-1$

		String path = dialog.open();
		if (path != null) {
			String content = buildHintFileContent(allDslRules);
			try {
				java.nio.file.Files.writeString(java.nio.file.Path.of(path), content);
			} catch (java.io.IOException e) {
				setContentDescription("Export failed: " + e.getMessage()); //$NON-NLS-1$
			}
		}
	}

	private List<String> collectSelectedDslRules() {
		List<String> rules = new ArrayList<>();
		Object input = commitTable.getInput();
		if (input instanceof CommitTableEntry[] entries) {
			for (CommitTableEntry entry : entries) {
				if (entry.hasRules()) {
					rules.addAll(entry.getDslRules());
				}
			}
		}
		return rules;
	}

	private static String buildHintFileContent(List<String> dslRules) {
		StringBuilder sb = new StringBuilder();
		sb.append("<!id: ai-inferred-rules>\n"); //$NON-NLS-1$
		sb.append("<!description: Rules inferred by AI from code changes>\n"); //$NON-NLS-1$
		sb.append("<!severity: info>\n"); //$NON-NLS-1$
		sb.append("<!tags: ai-inferred, mining>\n\n"); //$NON-NLS-1$
		for (String rule : dslRules) {
			sb.append(rule).append("\n;;\n\n"); //$NON-NLS-1$
		}
		return sb.toString();
	}

	// ---- git provider ----

	private static GitHistoryProvider createGitProvider() {
		try {
			return new JGitHistoryProvider();
		} catch (Exception e) {
			// Fallback to command-line git if JGit is not available
			return new CommandLineGitProvider();
		}
	}
}