AutoUnindentEditStrategy.java

/**
 * Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file). 
 * All Rights Reserved.
 * 
 * NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
 * OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
 * MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
 * 
 * This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 * SPDX-License-Identifier: EPL-2.0
 * 
 * Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
 * 
 * This program includes and/or can make use of certain third party source code, object code, documentation and other
 * files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
 * configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
 * conditions contained in any such Third Party Software or separate license file distributed with such Third Party
 * Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
 * aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
 * censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
 */
package org.osate.xtext.aadl2.ui.editor.autoedit;

import org.apache.log4j.Logger;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentCommand;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.xtext.formatting.IIndentationInformation;
import org.eclipse.xtext.resource.EObjectAtOffsetHelper;
import org.eclipse.xtext.ui.editor.autoedit.AbstractTerminalsEditStrategy;
import org.eclipse.xtext.ui.editor.model.IXtextDocument;
import org.osate.aadl2.AadlPackage;
import org.osate.aadl2.Classifier;
import org.osate.aadl2.ComponentCategory;
import org.osate.aadl2.NamedElement;
import org.osate.core.OsateCorePlugin;

import com.google.inject.Inject;
import com.google.inject.MembersInjector;

/**
 * This strategy applies auto edits when typing a newline character after a keyword.
 *
 * @author Lutz Wrage
 */
public class AutoUnindentEditStrategy extends AbstractTerminalsEditStrategy {

	public static class Factory {
		@Inject
		private MembersInjector<AutoUnindentEditStrategy> injector;
		@Inject
		private IIndentationInformation indentationInformation;

		public AutoUnindentEditStrategy newInstance(String terminal) {
			return newInstance(terminal, null);
		}

		public AutoUnindentEditStrategy newInstance(String terminal, String indentationString) {
			indentationString = indentationString == null ? indentationInformation.getIndentString()
					: indentationString;
			AutoUnindentEditStrategy strategy = new AutoUnindentEditStrategy(terminal, indentationString);
			injector.injectMembers(strategy);
			return strategy;
		}
	}

	@SuppressWarnings("unused")
	private final static Logger log = Logger.getLogger(AutoUnindentEditStrategy.class);

	private String indentationString;
	private IPreferenceStore store = OsateCorePlugin.getDefault().getPreferenceStore();

	public AutoUnindentEditStrategy(String terminal) {
		this(terminal, null);
	}

	public AutoUnindentEditStrategy(String terminal, String indentationString) {
		super(terminal, terminal);
		this.indentationString = indentationString;
	}

	@Override
	protected boolean allowsEqualTerminals() {
		return true;
	}

	private String findKeyWord(String[] tokens) {
		String keyword = "";
		if (tokens[0].equalsIgnoreCase("feature") && tokens[1].equalsIgnoreCase("group")) {
			keyword = tokens[0] + " " + tokens[1];
		} else if (tokens[0].equalsIgnoreCase("package")) {
			keyword = tokens[0];
		} else if (tokens[0].equalsIgnoreCase("property") && tokens[1].equalsIgnoreCase("set")) {
			keyword = "property set";
		} else if (ComponentCategory.getByName((tokens[0] + " " + tokens[1]).toLowerCase()) != null) {
			keyword = tokens[0] + " " + tokens[1];
		} else if (ComponentCategory.getByName(tokens[0].toLowerCase()) != null) {
			keyword = tokens[0];
		}
		return keyword;
	}

	private String findElementId(String[] tokens, String keyword) {
		String elementId = tokens[tokens.length - 1];
		for (int i = 0; i < tokens.length; i++) {
			if (tokens[i].equalsIgnoreCase("extends")) {
				elementId = tokens[i - 1];
			} else if (keyword.equalsIgnoreCase("property set") && tokens[i].equalsIgnoreCase("is")) {
				elementId = tokens[i - 1];
			}
		}
		return elementId;
	}

	private boolean checkForExistingEnd(String endWord, boolean hasExtends, String docText, String elementId,
			NamedElement namedElement, String keyword, String[] tokens, int lineOffset) {

		docText = docText.substring(lineOffset);
		String endName = " " + endWord + " ";
		if (hasExtends) {
			endName = endName + elementId;
		} else if (namedElement != null) {
			endName = endName + namedElement.getName().toLowerCase();
		} else {
			if (keyword.equals("property set")) {
				endName = endName + tokens[tokens.length - 2];
			} else {
				endName = endName + tokens[tokens.length - 1];
			}
		}
		docText = docText.replaceAll("--.*?" + System.lineSeparator(), System.lineSeparator());
		docText = docText.toLowerCase().replaceAll("\\s+", " ");
		int possibleEndIndex = docText.toLowerCase().indexOf(endName.toLowerCase());

		if (possibleEndIndex > -1) {
			int possibleEndEnd = possibleEndIndex + endName.length();
			for (int i = 0; possibleEndEnd + i < docText.length(); i++) {
				String possibleEnd = docText.toLowerCase().substring(possibleEndIndex, possibleEndEnd + i);
				if (possibleEnd.endsWith(";") || Character.isWhitespace(possibleEnd.charAt(possibleEnd.length() - 1))) {
					return true;
				} else if (!possibleEnd.equalsIgnoreCase(endName)) {
					return false;
				}
			}
			return true;
		}
		return false;
	}

	@Override
	protected void internalCustomizeDocumentCommand(IDocument document, DocumentCommand command)
			throws BadLocationException {

		if (!isAutoComplete() || !isLineDelimiter(document, command)) {
			return;
		}
		String publicWord = isUseCapitalization() ? "PUBLIC" + System.lineSeparator() + "\t" : "public";
		String endWord = isUseCapitalization() ? "END" : "end";
		int lineNr = document.getLineOfOffset(command.offset);
		int firstOffsetOfLine = document.getLineOffset(lineNr);
		int lineLength = document.getLineLength(lineNr);
		String lineText = document.get(firstOffsetOfLine, lineLength);
		String[] tokens = lineText.trim().split("\\s+");
		if (tokens.length < 2) {
			return;
		}
		String keyword = findKeyWord(tokens);
		boolean hasExtends = false;
		for (int i = 0; i < tokens.length; i++) {
			if (tokens[i].equalsIgnoreCase("extends")) {
				hasExtends = true;
			}
		}

		NamedElement namedElement = null;
		if (keyword.equals("")) {
			namedElement = null;
			if (document instanceof IXtextDocument) {
				IXtextDocument xDoc = (IXtextDocument) document;
				namedElement = xDoc.readOnly(resource -> {
					EObjectAtOffsetHelper helper = new EObjectAtOffsetHelper();
					EObject eobj = helper.resolveElementAt(resource, command.offset);
					if (eobj == null) {
						return null;
					} else if (eobj instanceof Classifier || eobj instanceof AadlPackage) {
						return (NamedElement) eobj;
					}
					return null;
				});
			}
		}
		String elementId = findElementId(tokens, keyword);

		if (namedElement == null && keyword.equals("") && !hasExtends) {
			return;
		} else if (checkForExistingEnd(endWord, hasExtends, document.get(), elementId, namedElement, keyword, tokens,
				firstOffsetOfLine)) {
			return;
		}

		if (ComponentCategory.getByName(elementId.toLowerCase()) != null) {
			return;
		}
		if (keyword == null || keyword.equals("") || elementId == null || elementId.equals("")) {
			return;
		}

		int firstTokenIndex = lineText.indexOf(tokens[0]);
		String leadingString = lineText.substring(0, firstTokenIndex);
		String indent = "";
		String targetString = "";
		if (keyword.equalsIgnoreCase("package")) {
			if (lineText.endsWith(System.lineSeparator())) {
				lineText = lineText.substring(0, lineText.indexOf(System.lineSeparator()));
			}
		} else if (keyword.equalsIgnoreCase("property set")) {
			if (lineText.endsWith(System.lineSeparator())) {
				lineText = lineText.substring(0, lineText.indexOf(System.lineSeparator()));
			}
			if (isAutoIndent()) {
				indent = "\t";
			}
		}

		targetString = buildTargetString(lineText, leadingString, endWord, elementId, keyword, publicWord, indent);

		if (keyword.equalsIgnoreCase("package")) {
			if (isUseCapitalization()) {
				command.text = "";
			}
			command.offset = command.offset + (System.lineSeparator() + leadingString + publicWord).length();
		} else if (keyword.equalsIgnoreCase("property set")) {
			command.offset = command.offset + (System.lineSeparator() + leadingString + indent).length();
			command.text = "";
		}
		document.replace(firstOffsetOfLine, lineText.length(), targetString);
	}

	private String buildTargetString(String lineText, String leadingString, String endWord, String elementId,
			String keyword, String publicWord, String indent) {
		StringBuilder targetStringBuilder = new StringBuilder();

		targetStringBuilder.append(lineText);

		if (keyword.equalsIgnoreCase("package")) {
			targetStringBuilder.append(System.lineSeparator());
			targetStringBuilder.append(leadingString);
			targetStringBuilder.append(publicWord);
			targetStringBuilder.append(System.lineSeparator());
		} else if (keyword.equalsIgnoreCase("property set")) {
			targetStringBuilder.append(System.lineSeparator());
			targetStringBuilder.append(leadingString);
			targetStringBuilder.append(indent);
			targetStringBuilder.append(System.lineSeparator());
		}

		targetStringBuilder.append(leadingString);
		targetStringBuilder.append(endWord);
		targetStringBuilder.append(" ");
		targetStringBuilder.append(elementId);
		targetStringBuilder.append(";");
		if ((!keyword.equalsIgnoreCase("package") && !keyword.equalsIgnoreCase("property set"))
				|| (keyword.equalsIgnoreCase("package") && (!isUseCapitalization()))) {
			targetStringBuilder.append(System.lineSeparator());
		}
		return targetStringBuilder.toString();
	}

	private boolean isLineDelimiter(IDocument document, DocumentCommand command) {
		if (command.length != 0) {
			return false;
		}
		String originalText = command.text;
		String[] lineDelimiters = document.getLegalLineDelimiters();
		int delimiterIndex = TextUtilities.startsWith(lineDelimiters, originalText);
		return delimiterIndex != -1 && originalText.trim().length() == 0;
	}

	protected boolean isAutoComplete() {
		return store.getBoolean(OsateCorePlugin.AUTO_COMPLETE);
	}

	protected boolean isAutoIndent() {
		return store.getBoolean(OsateCorePlugin.AUTO_INDENT);
	}

	protected boolean isUseCapitalization() {
		return store.getBoolean(OsateCorePlugin.CAPITALIZE);
	}

}