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.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.window.Window;
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.dialogs.ElementListSelectionDialog;
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>The selected repository and aggregate queue state are kept visible in the
 * view description so a potentially quota-consuming analysis never looks like
 * an unexplained background operation.</p>
 *
 * @since 1.2.6
 */
public class RefactoringMiningView extends ViewPart {

	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;
	private String activeRepositoryName = ""; //$NON-NLS-1$

	@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();
	}

	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);
	}

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

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

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

		mgr.add(new Separator());

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

	private void chooseAndAnalyzeProject() {
		List<IProject> projects = new ArrayList<>();
		for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) {
			if (project.isOpen() && project.getLocation() != null) {
				projects.add(project);
			}
		}
		if (projects.isEmpty()) {
			setContentDescription("No open workspace project is available for analysis"); //$NON-NLS-1$
			return;
		}

		ElementListSelectionDialog dialog = new ElementListSelectionDialog(getSite().getShell(), new LabelProvider() {
			@Override
			public String getText(Object element) {
				return element instanceof IProject project ? project.getName() : super.getText(element);
			}
		});
		dialog.setTitle("Select project for Refactoring Mining"); //$NON-NLS-1$
		dialog.setMessage("Choose the Git-backed workspace project to analyze:"); //$NON-NLS-1$
		dialog.setMultipleSelection(false);
		dialog.setElements(projects.toArray());
		if (dialog.open() != Window.OK || !(dialog.getFirstResult() instanceof IProject project)) {
			return;
		}

		activeRepositoryName = project.getName();
		setContentDescription("Repository: " + activeRepositoryName); //$NON-NLS-1$
		analyzeRepository(project.getLocation().toFile().toPath());
	}

	void analyzeRepository(Path repositoryPath) {
		if (scheduler != null) {
			scheduler.cancelAnalysis();
			scheduler = null;
		}
		if (activeRepositoryName.isBlank()) {
			Path fileName = repositoryPath.getFileName();
			activeRepositoryName = fileName != null ? fileName.toString() : repositoryPath.toString();
		}

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

			commitTable.setInput(entries.toArray(CommitTableEntry[]::new));
			detailPanel.showRules(null);

			EclipseLlmService llmService = EclipseLlmService.getInstance();
			if (!llmService.isAvailable()) {
				setContentDescription("Repository: " + activeRepositoryName //$NON-NLS-1$
						+ " — history loaded; LLM inference unavailable. " + llmService.configurationHint()); //$NON-NLS-1$
				return;
			}

			scheduler = new CommitAnalysisScheduler(gitProvider, repositoryPath, commitTable,
					this::updateProgressDescription);
			scheduler.startAnalysis(entries);
		} catch (Exception e) {
			setContentDescription("Repository: " + activeRepositoryName + " — error: " + e.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	private void updateProgressDescription(CommitAnalysisScheduler.Progress progress) {
		String prefix = "Repository: " + activeRepositoryName + " — "; //$NON-NLS-1$ //$NON-NLS-2$
		if (progress.cancelled() && !progress.running()) {
			setContentDescription(prefix + "cancelled after " + progress.completed() + "/" + progress.total()); //$NON-NLS-1$ //$NON-NLS-2$
		} else if (progress.running()) {
			setContentDescription(prefix + "analyzed " + progress.completed() + "/" + progress.total() //$NON-NLS-1$ //$NON-NLS-2$
					+ " — active " + progress.active() + " — queued " + progress.queued()); //$NON-NLS-1$ //$NON-NLS-2$
		} else if (progress.total() == 0) {
			setContentDescription(prefix + "no commits to analyze"); //$NON-NLS-1$
		} else {
			setContentDescription(prefix + "analyzed " + progress.completed() + "/" + progress.total() //$NON-NLS-1$ //$NON-NLS-2$
					+ " — finished"); //$NON-NLS-1$
		}
	}

	private void exportAsHintFile() {
		List<String> selectedDslRules = detailPanel.getSelectedDslRules();
		if (selectedDslRules.isEmpty()) {
			setContentDescription("Repository: " + activeRepositoryName //$NON-NLS-1$
					+ " — select a commit and check the rules to export"); //$NON-NLS-1$
			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(selectedDslRules);
			try {
				java.nio.file.Files.writeString(java.nio.file.Path.of(path), content);
			} catch (java.io.IOException e) {
				setContentDescription("Repository: " + activeRepositoryName + " — export failed: " //$NON-NLS-1$ //$NON-NLS-2$
						+ e.getMessage());
			}
		}
	}

	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();
	}

	private static GitHistoryProvider createGitProvider() {
		try {
			return new JGitHistoryProvider();
		} catch (Exception e) {
			return new CommandLineGitProvider();
		}
	}
}