CSSCleanupAction.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.css.ui;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IObjectActionDelegate;
import org.eclipse.ui.IWorkbenchPart;

import org.sandbox.jdt.internal.css.CSSCleanupPlugin;
import org.sandbox.jdt.internal.css.core.CSSValidationResult;
import org.sandbox.jdt.internal.css.core.NodeExecutor;
import org.sandbox.jdt.internal.css.core.PrettierRunner;
import org.sandbox.jdt.internal.css.core.StylelintRunner;
import org.sandbox.jdt.internal.css.preferences.CSSPreferenceConstants;

/**
 * Action to format and validate CSS files.
 */
public class CSSCleanupAction implements IObjectActionDelegate {

	private ISelection selection;
	private IWorkbenchPart targetPart;

	@Override
	public void run(IAction action) {
		if (!(selection instanceof IStructuredSelection)) {
			return;
		}

		Shell shell = targetPart.getSite().getShell();

		// Check if Node.js is available
		if (!NodeExecutor.isNodeAvailable()) {
			MessageDialog.openError(shell, "CSS Cleanup", //$NON-NLS-1$
					"Node.js is not installed or not in PATH.\n\n" + //$NON-NLS-1$
							"Please install Node.js from https://nodejs.org/\n" + //$NON-NLS-1$
							"and ensure 'node' and 'npx' are available in your PATH."); //$NON-NLS-1$
			return;
		}

		IStructuredSelection structuredSelection = (IStructuredSelection) selection;

		// Process each CSS file in a background job
		for (Object element : structuredSelection.toList()) {
			IResource resource = Adapters.adapt(element, IResource.class);
			if (resource instanceof IFile) {
				IFile file = (IFile) resource;
				if (isCSSFile(file)) {
					scheduleProcessingJob(file, shell);
				}
			}
		}
	}

	/**
	 * Schedule a background job to process the CSS file.
	 */
	private void scheduleProcessingJob(IFile file, Shell shell) {
		Job job = new Job("CSS Cleanup: " + file.getName()) { //$NON-NLS-1$
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				try {
					monitor.beginTask("Processing " + file.getName(), 100); //$NON-NLS-1$
					
					IPreferenceStore store = CSSCleanupPlugin.getDefault().getPreferenceStore();
					boolean enablePrettier = store.getBoolean(CSSPreferenceConstants.ENABLE_PRETTIER);
					boolean enableStylelint = store.getBoolean(CSSPreferenceConstants.ENABLE_STYLELINT);

					// Format with Prettier if enabled
					if (enablePrettier && !monitor.isCanceled()) {
						monitor.subTask("Formatting with Prettier..."); //$NON-NLS-1$
						String formatted = PrettierRunner.format(file);
						monitor.worked(50);

						if (formatted != null && !monitor.isCanceled()) {
							// Write back to file
							file.setContents(
									new java.io.ByteArrayInputStream(formatted.getBytes(java.nio.charset.StandardCharsets.UTF_8)),
									IResource.KEEP_HISTORY,
									null);
							file.refreshLocal(IResource.DEPTH_ZERO, null);
						}
					} else {
						monitor.worked(50);
					}

					// Validate with Stylelint if enabled
					if (enableStylelint && !monitor.isCanceled()) {
						monitor.subTask("Validating with Stylelint..."); //$NON-NLS-1$
						CSSValidationResult result = StylelintRunner.validate(file);
						monitor.worked(50);

						if (!result.isValid() && !monitor.isCanceled()) {
							// Show validation warnings on UI thread
							Display.getDefault().asyncExec(() -> {
								StringBuilder msg = new StringBuilder("CSS validation issues:%n%n"); //$NON-NLS-1$
								for (CSSValidationResult.Issue issue : result.getIssues()) {
									msg.append(String.format("Line %d: [%s] %s%n", //$NON-NLS-1$
											Integer.valueOf(issue.line), issue.severity, issue.message));
								}
								MessageDialog.openWarning(shell, "CSS Validation", msg.toString()); //$NON-NLS-1$
							});
						}
					} else {
						monitor.worked(50);
					}

					monitor.done();
					return Status.OK_STATUS;

				} catch (Exception e) {
					// Show error dialog on UI thread
					Display.getDefault().asyncExec(() -> {
						MessageDialog.openError(shell, "CSS Cleanup Error", //$NON-NLS-1$
								"Failed to process CSS file: " + e.getMessage()); //$NON-NLS-1$
					});
					return new Status(IStatus.ERROR, CSSCleanupPlugin.PLUGIN_ID,
							"Failed to process CSS file", e); //$NON-NLS-1$
				}
			}
		};
		job.setUser(true); // Show in progress view
		job.schedule();
	}

	private boolean isCSSFile(IFile file) {
		String ext = file.getFileExtension();
		return ext != null && (ext.equals("css") || ext.equals("scss") || ext.equals("less")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}



	@Override
	public void selectionChanged(IAction action, ISelection selection) {
		this.selection = selection;
	}

	@Override
	public void setActivePart(IAction action, IWorkbenchPart targetPart) {
		this.targetPart = targetPart;
	}
}