DslContentAssistProcessor.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.editor;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;

/**
 * Content assist processor for {@code .sandbox-hint} files that provides
 * completions for TriggerPattern DSL keywords.
 *
 * <p>Keyword categories:</p>
 * <ul>
 *   <li><b>Metadata directives</b>: {@code <!id:>}, {@code <!description:>}, etc.</li>
 *   <li><b>Guard functions</b>: {@code instanceof}, {@code matchesAny}, etc.</li>
 *   <li><b>Rule separators</b>: {@code =>}, {@code ;;}, {@code otherwise}</li>
 * </ul>
 *
 * @since 1.2.6
 */
public class DslContentAssistProcessor implements IContentAssistProcessor {

	private static final String[] METADATA_DIRECTIVES = {
			"<!id:>", //$NON-NLS-1$
			"<!description:>", //$NON-NLS-1$
			"<!severity:>", //$NON-NLS-1$
			"<!minJavaVersion:>", //$NON-NLS-1$
			"<!tags:>", //$NON-NLS-1$
			"<!include:>" //$NON-NLS-1$
	};

	private static final String[] GUARD_FUNCTIONS = {
			"instanceof", //$NON-NLS-1$
			"matchesAny", //$NON-NLS-1$
			"matchesNone", //$NON-NLS-1$
			"hasNoSideEffect", //$NON-NLS-1$
			"sourceVersionGE", //$NON-NLS-1$
			"sourceVersionLE", //$NON-NLS-1$
			"sourceVersionBetween", //$NON-NLS-1$
			"isStatic", //$NON-NLS-1$
			"isFinal", //$NON-NLS-1$
			"hasAnnotation", //$NON-NLS-1$
			"isDeprecated", //$NON-NLS-1$
			"referencedIn", //$NON-NLS-1$
			"elementKindMatches", //$NON-NLS-1$
			"contains", //$NON-NLS-1$
			"notContains", //$NON-NLS-1$
			"methodNameMatches" //$NON-NLS-1$
	};

	private static final String[] RULE_SEPARATORS = {
			"=>", //$NON-NLS-1$
			";;", //$NON-NLS-1$
			"otherwise" //$NON-NLS-1$
	};

	@Override
	public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
		String text = viewer.getDocument().get();
		String prefix = extractPrefix(text, offset);
		String prefixLower = prefix.toLowerCase();

		List<ICompletionProposal> proposals = new ArrayList<>();
		addMatchingProposals(METADATA_DIRECTIVES, "directive", prefix, prefixLower, offset, proposals); //$NON-NLS-1$
		addMatchingProposals(GUARD_FUNCTIONS, "guard", prefix, prefixLower, offset, proposals); //$NON-NLS-1$
		addMatchingProposals(RULE_SEPARATORS, "separator", prefix, prefixLower, offset, proposals); //$NON-NLS-1$
		return proposals.toArray(new ICompletionProposal[0]);
	}

	@Override
	public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
		return new IContextInformation[0];
	}

	@Override
	public char[] getCompletionProposalAutoActivationCharacters() {
		return new char[] { '<', '!' };
	}

	@Override
	public char[] getContextInformationAutoActivationCharacters() {
		return null;
	}

	@Override
	public IContextInformationValidator getContextInformationValidator() {
		return null;
	}

	@Override
	public String getErrorMessage() {
		return null;
	}

	private static String extractPrefix(String text, int offset) {
		int start = offset;
		while (start > 0) {
			char ch = text.charAt(start - 1);
			if (Character.isWhitespace(ch)) {
				break;
			}
			start--;
		}
		return text.substring(start, offset);
	}

	private static void addMatchingProposals(String[] keywords, String category,
			String prefix, String prefixLower, int offset,
			List<ICompletionProposal> proposals) {
		for (String keyword : keywords) {
			if (prefix.isEmpty() || keyword.toLowerCase().startsWith(prefixLower)) {
				int replacementOffset = offset - prefix.length();
				proposals.add(new CompletionProposal(
						keyword,
						replacementOffset,
						prefix.length(),
						keyword.length(),
						null,
						keyword + " - " + category, //$NON-NLS-1$
						null,
						null));
			}
		}
	}
}