EncodingSourceRewrite.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer and others.
 *
 * 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
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.cleanup;

import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.compiler.InvalidInputException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.LineComment;
import org.eclipse.jdt.core.refactoring.CompilationUnitChange;
import org.eclipse.jdt.internal.corext.refactoring.nls.NLSElement;
import org.eclipse.jdt.internal.corext.refactoring.nls.NLSLine;
import org.eclipse.jdt.internal.corext.refactoring.nls.NLSScanner;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.jdt.internal.corext.refactoring.util.TextEditUtil;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.triggerpattern.cleanup.EncodingSourceEdits.Edit;

/** Adds NLS edits to the completed AST edit tree, including moved source ranges. */
public final class EncodingSourceRewrite {

	private static final String REPLACEMENTS= EncodingSourceRewrite.class.getName() + ".replacements"; //$NON-NLS-1$

	private record Replacements(CompilationUnitRewrite rewrite, Set<ASTNode> nodes) {
	}

	private EncodingSourceRewrite() {
	}

	public static void clear(CompilationUnit root) {
		root.setProperty(REPLACEMENTS, null);
	}

	public static void record(CompilationUnitRewrite cuRewrite, ASTNode argument) {
		Object stored= cuRewrite.getRoot().getProperty(REPLACEMENTS);
		Replacements replacements;
		if (stored instanceof Replacements previous && previous.rewrite() == cuRewrite) {
			replacements= previous;
		} else {
			replacements= new Replacements(cuRewrite, Collections.newSetFromMap(new IdentityHashMap<>()));
			cuRewrite.getRoot().setProperty(REPLACEMENTS, replacements);
		}
		replacements.nodes().add(argument);
	}

	public static void complete(CompilationUnit root, CompilationUnitChange change) throws JavaModelException {
		if (!(root.getProperty(REPLACEMENTS) instanceof Replacements replacements)) {
			return;
		}
		CompilationUnitRewrite cuRewrite= replacements.rewrite();
		String buffer= cuRewrite.getCu().getBuffer().getContents();
		Map<LineComment, List<Edit>> commentEdits= new LinkedHashMap<>();
		for (NLSLine line : scan(cuRewrite, buffer)) {
			int removed= 0;
			NLSElement[] elements= line.getElements();
			for (int index= 0; index < elements.length; index++) {
				NLSElement element= elements[index];
				boolean deleted= isRemoved(element.getPosition().getOffset(), element.getPosition().getLength(), replacements.nodes());
				if (deleted) {
					removed++;
				}
				if (!element.hasTag() || (!deleted && removed == 0)) {
					continue;
				}
				int tagStart= element.getTagPosition().getOffset();
				int tagLength= element.getTagPosition().getLength();
				if (isRemoved(tagStart, tagLength, replacements.nodes())) {
					continue;
				}
				LineComment comment= containingComment(root, tagStart, tagLength);
				String tag= deleted ? "" : NLSElement.createTagText(index + 1 - removed); //$NON-NLS-1$
				commentEdits.computeIfAbsent(comment, key -> new ArrayList<>())
						.add(new Edit(tagStart - comment.getStartPosition(), tagLength, tag));
			}
		}
		commentEdits.forEach((comment, tags) -> {
			int start= comment.getStartPosition();
			int end= start + comment.getLength();
			while (end > start && (buffer.charAt(end - 1) == '\n' || buffer.charAt(end - 1) == '\r')) {
				end--;
			}
			String text= EncodingSourceEdits.rewriteComment(buffer.substring(start, end), tags);
			if (text.isEmpty()) {
				while (start > 0 && EncodingSourceEdits.isHorizontalSpace(buffer.charAt(start - 1))) {
					start--;
				}
			}
			ReplaceEdit edit= new ReplaceEdit(start, end - start, text);
			// Inserting into the enclosing MoveSourceEdit applies the tag change to
			// the moved text too. Adding it only at the root would lose that change.
			TextEditUtil.insert(change.getEdit(), edit);
			change.addTextEditGroup(new TextEditGroup(change.getName(), edit));
		});
	}

	private static NLSLine[] scan(CompilationUnitRewrite cuRewrite, String buffer) throws JavaModelException {
		try {
			NLSLine[] lines= NLSScanner.scan(cuRewrite.getCu());
			if (!buffer.equals(cuRewrite.getCu().getBuffer().getContents())) {
				throw new IllegalArgumentException("Source changed while scanning encoding NLS tags"); //$NON-NLS-1$
			}
			return lines;
		} catch (InvalidInputException | BadLocationException exception) {
			throw new IllegalArgumentException("Cannot safely determine encoding NLS tag positions", exception); //$NON-NLS-1$
		}
	}

	private static boolean isRemoved(int offset, int length, Set<ASTNode> replacements) {
		return replacements.stream().anyMatch(node -> offset >= node.getStartPosition()
				&& offset + length <= node.getStartPosition() + node.getLength());
	}

	private static LineComment containingComment(CompilationUnit root, int offset, int length) {
		for (Object candidate : root.getCommentList()) {
			if (candidate instanceof LineComment comment && offset >= comment.getStartPosition()
					&& offset + length <= comment.getStartPosition() + comment.getLength()) {
				return comment;
			}
		}
		throw new IllegalArgumentException("NLS tag is not inside a source line comment"); //$NON-NLS-1$
	}
}