SandboxHintOrganizeImports.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 - initial API and implementation
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.editor;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.jface.text.IDocument;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;

/**
 * Organizes imports in {@code <? ?>} blocks of a {@code .sandbox-hint} file.
 *
 * <p>Strategy:</p>
 * <ol>
 *   <li>Find all {@code <? ?>} blocks in the document</li>
 *   <li>Extract import statements from each block</li>
 *   <li>Sort imports alphabetically</li>
 *   <li>Remove duplicate imports</li>
 *   <li>Write organized imports back into the {@code <? ?>} block</li>
 * </ol>
 *
 * <p>Imports outside of {@code <? ?>} blocks are not changed.</p>
 *
 * @since 1.5.0
 */
public class SandboxHintOrganizeImports {

	private static final Pattern IMPORT_PATTERN =
			Pattern.compile("^\\s*import\\s+(?:static\\s+)?[\\w.]+(?:\\.\\*)?\\s*;\\s*$"); //$NON-NLS-1$

	private static final Pattern EMBEDDED_BLOCK_PATTERN =
			Pattern.compile("<\\?([\\s\\S]*?)\\?>"); //$NON-NLS-1$

	/**
	 * Organizes imports in all {@code <? ?>} blocks of the document.
	 *
	 * @param document the document to organize imports in
	 * @return a {@link TextEdit} with all changes, or {@code null} if no changes needed
	 */
	public TextEdit organizeImports(IDocument document) {
		if (document == null) {
			return null;
		}

		String content;
		try {
			content = document.get();
		} catch (RuntimeException e) {
			return null;
		}

		MultiTextEdit multiEdit = new MultiTextEdit();
		Matcher blockMatcher = EMBEDDED_BLOCK_PATTERN.matcher(content);

		while (blockMatcher.find()) {
			int blockStart = blockMatcher.start(1);
			String blockContent = blockMatcher.group(1);

			TextEdit blockEdit = organizeBlockImports(blockContent, blockStart);
			if (blockEdit != null) {
				multiEdit.addChild(blockEdit);
			}
		}

		if (!multiEdit.hasChildren()) {
			return null;
		}
		return multiEdit;
	}

	/**
	 * Organizes imports within a single {@code <? ?>} block.
	 */
	private TextEdit organizeBlockImports(String blockContent, int blockStartOffset) {
		String[] lines = blockContent.split("\\n"); //$NON-NLS-1$
		List<String> imports = new ArrayList<>();
		int importRegionStart = -1;
		int importRegionEnd = -1;
		int currentOffset = 0;

		for (int i = 0; i < lines.length; i++) {
			String line = lines[i];
			if (IMPORT_PATTERN.matcher(line).matches()) {
				String importText = line.trim();
				if (!imports.contains(importText)) {
					imports.add(importText);
				}
				if (importRegionStart == -1) {
					importRegionStart = currentOffset;
				}
				importRegionEnd = currentOffset + line.length();
			}
			currentOffset += line.length() + 1; // +1 for newline
		}

		if (imports.isEmpty() || importRegionStart == -1) {
			return null;
		}

		// Sort imports alphabetically
		imports.sort(String::compareTo);

		String organizedImports = String.join("\n", imports); //$NON-NLS-1$
		String originalRegion = blockContent.substring(importRegionStart, importRegionEnd);

		if (organizedImports.equals(originalRegion)) {
			return null; // No changes needed
		}

		return new ReplaceEdit(blockStartOffset + importRegionStart,
				importRegionEnd - importRegionStart, organizedImports);
	}
}