PlaceholderRule.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 org.eclipse.jface.text.rules.ICharacterScanner;
import org.eclipse.jface.text.rules.IRule;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.Token;

/**
 * Rule that matches placeholder tokens in {@code .sandbox-hint} files.
 *
 * <p>Matches two forms:</p>
 * <ul>
 *   <li>{@code $name} – single placeholder (uses {@code placeholderToken})</li>
 *   <li>{@code $name$} – variadic placeholder (uses {@code variadicToken})</li>
 * </ul>
 *
 * @since 1.3.6
 */
public class PlaceholderRule implements IRule {

	private final IToken placeholderToken;
	private final IToken variadicToken;

	/**
	 * Creates a placeholder rule.
	 *
	 * @param placeholderToken token for single placeholders ({@code $name})
	 * @param variadicToken token for variadic placeholders ({@code $name$})
	 */
	public PlaceholderRule(IToken placeholderToken, IToken variadicToken) {
		this.placeholderToken = placeholderToken;
		this.variadicToken = variadicToken;
	}

	@Override
	public IToken evaluate(ICharacterScanner scanner) {
		int c = scanner.read();
		if (c != '$') {
			scanner.unread();
			return Token.UNDEFINED;
		}

		// Read the placeholder name
		int length = 1; // '$' already read
		boolean hasNameChars = false;
		while (true) {
			c = scanner.read();
			length++;
			if (c == ICharacterScanner.EOF) {
				// Unread all and fail
				for (int i = 0; i < length; i++) {
					scanner.unread();
				}
				return Token.UNDEFINED;
			}
			if (c == '$') {
				// Variadic: $name$
				if (hasNameChars) {
					return variadicToken;
				}
				// Just "$$" - not a valid placeholder
				for (int i = 0; i < length; i++) {
					scanner.unread();
				}
				return Token.UNDEFINED;
			}
			if (Character.isLetterOrDigit((char) c) || c == '_') {
				hasNameChars = true;
			} else {
				// End of placeholder name
				scanner.unread();
				if (hasNameChars) {
					return placeholderToken;
				}
				// Just '$' followed by non-name char
				scanner.unread(); // unread the '$'
				return Token.UNDEFINED;
			}
		}
	}
}