All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fife.rsta.ac.css.PropertyValueCompletionProvider Maven / Gradle / Ivy

/*
 * 11/28/2013
 *
 * Copyright (C) 2013 Robert Futrell
 * robert_futrell at users.sourceforge.net
 * http://fifesoft.com/rsyntaxtextarea
 *
 * This library is distributed under a modified BSD license.  See the included
 * LICENSE.md file for details.
 */
package org.fife.rsta.ac.css;

import java.awt.Point;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.Segment;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.fife.ui.autocomplete.AbstractCompletionProvider;
import org.fife.ui.autocomplete.Completion;
import org.fife.ui.autocomplete.CompletionProviderBase;
import org.fife.ui.autocomplete.CompletionXMLParser;
import org.fife.ui.autocomplete.ParameterizedCompletion;
import org.fife.ui.autocomplete.Util;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.Token;
import org.xml.sax.SAXException;


/**
 * The completion provider used for CSS properties and values.
 *
 * @author Robert Futrell
 * @version 1.0
 */
public class PropertyValueCompletionProvider extends CompletionProviderBase {

	private List htmlTagCompletions;
	private List propertyCompletions;
	private Map> valueCompletions;
	private Map> valueCompletionGenerators;

	private Segment seg = new Segment();
	private AbstractCompletionProvider.CaseInsensitiveComparator comparator;

	/**
	 * If we're going to display value completions for a property, this is the
	 * property to do it for.
	 */
	private String currentProperty;

	private boolean isLess;


	/**
	 * The most common vendor prefixes.  We ignore these.
	 */
	private static final Pattern VENDOR_PREFIXES =
			Pattern.compile("^\\-(?:ms|moz|o|xv|webkit|khtml|apple)\\-");

	private final Completion inheritCompletion =
		new BasicCssCompletion(this, "inherit", "css_propertyvalue_identifier");


	public PropertyValueCompletionProvider(boolean isLess) {

		setAutoActivationRules(true, "@: ");
		// While we don't have functions per-se in CSS, we do in Less
		setParameterizedCompletionParams('(', ", ", ')');
		this.isLess = isLess;

		try {
			this.valueCompletions = new HashMap<>();
			this.valueCompletionGenerators =
                    new HashMap<>();
			loadPropertyCompletions();
			this.htmlTagCompletions = loadHtmlTagCompletions();
		} catch (IOException ioe) { // Never happens
			throw new RuntimeException(ioe);
		}

		comparator = new AbstractCompletionProvider.CaseInsensitiveComparator();

	}


	private void addAtRuleCompletions(List completions) {
		completions.add(new BasicCssCompletion(this, "@charset", "charset_rule"));
		completions.add(new BasicCssCompletion(this, "@import", "link_rule"));
		completions.add(new BasicCssCompletion(this, "@namespace", "charset_rule"));
		completions.add(new BasicCssCompletion(this, "@media", "media_rule"));
		completions.add(new BasicCssCompletion(this, "@page", "page_rule"));
		completions.add(new BasicCssCompletion(this, "@font-face", "fontface_rule"));
		completions.add(new BasicCssCompletion(this, "@keyframes", "charset_rule"));
		completions.add(new BasicCssCompletion(this, "@supports", "charset_rule"));
		completions.add(new BasicCssCompletion(this, "@document", "charset_rule"));
	}


	@Override
	public String getAlreadyEnteredText(JTextComponent comp) {

		Document doc = comp.getDocument();

		int dot = comp.getCaretPosition();
		Element root = doc.getDefaultRootElement();
		int index = root.getElementIndex(dot);
		Element elem = root.getElement(index);
		int start = elem.getStartOffset();
		int len = dot-start;
		try {
			doc.getText(start, len, seg);
		} catch (BadLocationException ble) {
			ble.printStackTrace();
			return EMPTY_STRING;
		}

		int segEnd = seg.offset + len;
		start = segEnd - 1;
		while (start>=seg.offset && isValidChar(seg.array[start])) {
			start--;
		}
		start++;

		len = segEnd - start;
		if (len==0) {
			return EMPTY_STRING;
		}

		String text = new String(seg.array, start, len);
		return removeVendorPrefix(text);
	}


	private static String removeVendorPrefix(String text) {
		if (text.length()>0 && text.charAt(0)=='-') {
			Matcher m = VENDOR_PREFIXES.matcher(text);
			if (m.find()) {
				text = text.substring(m.group().length());
			}
		}
		return text;
	}


	@Override
	public List getCompletionsAt(JTextComponent comp, Point p) {
		// TODO Auto-generated method stub
		return null;
	}


	@Override
	public List getParameterizedCompletions(
			JTextComponent tc) {
		return null;
	}


	private LexerState getLexerState(RSyntaxTextArea textArea, int line) {

		int dot = textArea.getCaretPosition();
		LexerState state = LexerState.SELECTOR;
		boolean somethingFound = false;
		currentProperty = null;

		while (line>=0 && !somethingFound) {
			Token t = textArea.getTokenListForLine(line--);
			while (t!=null && t.isPaintable() && !t.containsPosition(dot)) {
				if (t.getType()==Token.RESERVED_WORD) {
					state = LexerState.PROPERTY;
					currentProperty = removeVendorPrefix(t.getLexeme());
					somethingFound = true;
				}
				else if (!isLess && t.getType() == Token.VARIABLE) {
					// TokenTypes.VARIABLE == IDs in CSS, variables in Less
					state = LexerState.SELECTOR;
					currentProperty = null;
					somethingFound = true;
				}
				else if (t.getType()==Token.PREPROCESSOR ||
						t.getType()==Token.FUNCTION ||
						t.getType()==Token.LITERAL_NUMBER_DECIMAL_INT) {
					state = LexerState.VALUE;
					somethingFound = true;
				}
				else if (t.isLeftCurly()) {
					state = LexerState.PROPERTY;
					somethingFound = true;
				}
				else if (t.isRightCurly()) {
					state = LexerState.SELECTOR;
					currentProperty = null;
					somethingFound = true;
				}
				else if (t.isSingleChar(Token.OPERATOR, ':')) {
					state = LexerState.VALUE;
					somethingFound = true;
				}
				else if (t.isSingleChar(Token.OPERATOR, ';')) {
					state = LexerState.PROPERTY;
					currentProperty = null;
					somethingFound = true;
				}
				t = t.getNextToken();
			}
		}

		return state;

	}


	@Override
	@SuppressWarnings("unchecked")
	protected List getCompletionsImpl(JTextComponent comp) {

		List retVal = new ArrayList<>();
		String text = getAlreadyEnteredText(comp);

		if (text!=null) {

			// Our completion choices depend on where we are in the CSS
			RSyntaxTextArea textArea = (RSyntaxTextArea)comp;
			LexerState lexerState = getLexerState(textArea,
					textArea.getCaretLineNumber());

			List choices = new ArrayList<>();
			switch (lexerState) {
				case SELECTOR:
					choices = htmlTagCompletions;
					break;
				case PROPERTY:
					choices = propertyCompletions;
					break;
				case VALUE:
					choices = valueCompletions.get(currentProperty);
					List generators =
							valueCompletionGenerators.get(currentProperty);
					if (generators!=null) {
						for (CompletionGenerator generator : generators) {
							List toMerge = generator.
													generate(this, text);
							if (toMerge!=null) {
								if (choices==null) {
									choices = toMerge;
								}
								else {
									// Clone choices array since we had a shallow
									// copy of the "static" completions for this
									// property
									choices = new ArrayList<>(choices);
									choices.addAll(toMerge);
								}
							}
						}
					}
					if (choices==null) {
						choices = new ArrayList<>();
					}
					Collections.sort(choices);
					break;
			}

			if (isLess) {
				if (addLessCompletions(choices, lexerState, comp, text)) {
					Collections.sort(choices);
				}
			}

			int index = Collections.binarySearch(choices, text, comparator);
			if (index<0) { // No exact match
				index = -index - 1;
			}
			else {
				// If there are several overloads for the function being
				// completed, Collections.binarySearch() will return the index
				// of one of those overloads, but we must return all of them,
				// so search backward until we find the first one.
				int pos = index - 1;
				while (pos>0 &&
						comparator.compare(choices.get(pos), text)==0) {
					retVal.add(choices.get(pos));
					pos--;
				}
			}

			while (index completions,
			LexerState state, JTextComponent comp, String alreadyEntered) {
		return false;
	}


	@Override
	public boolean isAutoActivateOkay(JTextComponent tc) {

		boolean ok = super.isAutoActivateOkay(tc);

		// In our constructor, we set up auto-activation of the completion
		// popup to occur on space chars.  This extra check makes it a little
		// more sane, by only letting space auto-activate completion choices
		// for property values.
		if (ok) {
			RSyntaxDocument doc = (RSyntaxDocument)tc.getDocument();
			int dot = tc.getCaretPosition();
			try {
				if (dot>1 && doc.charAt(dot)==' ') { // Caret hasn't advanced (?)
					ok = doc.charAt(dot-1)==':';
				}
			} catch (BadLocationException ble) {
				ble.printStackTrace(); // Never happens
			}
		}

		return ok;

	}


	/**
	 * Returns whether a character is valid in a property value.
	 *
	 * @param ch The character.
	 * @return Whether it is valid.
	 */
	public boolean isValidChar(char ch) {
		switch (ch) {
			case '-':
			case '_':
			case '#':
			case '.':
			case '@':
				return true;
		}
		return Character.isLetterOrDigit(ch);
	}


	private List loadHtmlTagCompletions() throws IOException {

		// TODO: Share/grab this list directly from HtmlCompletionProvider?
		List completions = loadFromXML("data/html.xml");

		addAtRuleCompletions(completions);

		Collections.sort(completions);
		return completions;

	}


	private void loadPropertyCompletions() throws IOException {

		propertyCompletions = new ArrayList<>();

		BufferedReader r;
		ClassLoader cl = getClass().getClassLoader();
		InputStream in = cl.getResourceAsStream("data/css_properties.txt");
		if (in!=null) {
			r = new BufferedReader(new InputStreamReader(in));
		}
		else {
			r = new BufferedReader(new FileReader("data/css_properties.txt"));
		}
		String line;
		try {
			while ((line=r.readLine())!=null) {
				if (line.length()>0 && line.charAt(0)!='#') {
					parsePropertyValueCompletionLine(line);
				}
			}
		} finally {
			r.close();
		}

		Collections.sort(propertyCompletions);

	}


	/**
	 * Loads completions from an XML input stream.  The XML should validate
	 * against CompletionXml.dtd.
	 *
	 * @param in The input stream to read from.
	 * @param cl The class loader to use when loading any extra classes defined
	 *        in the XML, such as custom {@code FunctionCompletion}s.  This
	 *        may be null if the default is to be used, or if no
	 *        custom completions are defined in the XML.
	 * @throws IOException If an IO error occurs.
	 */
	private List loadFromXML(InputStream in, ClassLoader cl)
			throws IOException {

		List completions;

		SAXParserFactory factory = SAXParserFactory.newInstance();
		factory.setValidating(true);
		CompletionXMLParser handler = new CompletionXMLParser(this, cl);
        try (BufferedInputStream bin = new BufferedInputStream(in)) {
            SAXParser saxParser = factory.newSAXParser();
            saxParser.parse(bin, handler);
            completions = handler.getCompletions();
            // Ignore parameterized completion params
        } catch (SAXException | ParserConfigurationException e) {
            throw new IOException(e.toString());
        }

        return completions;
	}


	/**
	 * Loads completions from an XML file.  The XML should validate against
	 * CompletionXml.dtd.
	 *
	 * @param resource A resource the current ClassLoader can get to.
	 * @throws IOException If an IO error occurs.
	 */
	protected List loadFromXML(String resource) throws IOException {
		ClassLoader cl = getClass().getClassLoader();
		InputStream in = cl.getResourceAsStream(resource);
		if (in==null) {
			File file = new File(resource);
			if (file.isFile()) {
				in = new FileInputStream(file);
			}
			else {
				throw new IOException("No such resource: " + resource);
			}
		}
        try (BufferedInputStream bin = new BufferedInputStream(in)) {
            return loadFromXML(bin, null);
        }
	}


	/**
	 * Adds a completion generator for a specific property.
	 */
	private static void add(
			Map> generatorMap,
			String prop, CompletionGenerator generator) {
        List generators = generatorMap.computeIfAbsent(prop, k -> new ArrayList<>());
        generators.add(generator);
	}


	private void parsePropertyValueCompletionLine(String line) {

		String[] tokens = line.split("\\s+");
		String prop = tokens[0];
		String icon = tokens.length>1 ? tokens[1] : null;
		propertyCompletions.add(new PropertyCompletion(this, prop, icon));

		if (tokens.length>2) {

			List completions = new ArrayList<>();
			completions.add(inheritCompletion);

			// Format: display gifname [ none inline block ]
			if (tokens[2].equals("[") &&
					tokens[tokens.length-1].equals("]")) {
				for (int i=3; i




© 2015 - 2024 Weber Informatics LLC | Privacy Policy