blob: 72f433779aaba246dc905fb0b77e093a85d3eb11 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.html.custom;
import org.netbeans.modules.html.custom.hints.CheckerElementVisitor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.editor.mimelookup.MimeRegistrations;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.modules.csl.api.Error;
import org.netbeans.modules.csl.api.Hint;
import org.netbeans.modules.csl.api.HintsProvider;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.api.RuleContext;
import org.netbeans.modules.html.custom.conf.Configuration;
import org.netbeans.modules.html.custom.conf.Tag;
import org.netbeans.modules.html.custom.hints.CustomElementHint;
import org.netbeans.modules.html.editor.api.gsf.HtmlExtension;
import org.netbeans.modules.html.editor.api.gsf.HtmlParserResult;
import org.netbeans.modules.html.editor.lib.api.HtmlSource;
import org.netbeans.modules.html.editor.lib.api.SyntaxAnalyzerResult;
import org.netbeans.modules.html.editor.lib.api.elements.Attribute;
import org.netbeans.modules.html.editor.lib.api.elements.Element;
import org.netbeans.modules.html.editor.lib.api.elements.ElementType;
import org.netbeans.modules.html.editor.lib.api.elements.ElementUtils;
import org.netbeans.modules.html.editor.lib.api.elements.Named;
import org.netbeans.modules.html.editor.lib.api.elements.Node;
import org.netbeans.modules.html.editor.lib.api.elements.OpenTag;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.spi.editor.completion.CompletionItem;
import org.openide.filesystems.FileObject;
import org.openide.util.Pair;
/**
*
* @author marek
*/
@MimeRegistrations({
@MimeRegistration(mimeType = "text/html", service = HtmlExtension.class)
})
public class CustomHtmlExtension extends HtmlExtension {
private Pair<HtmlSource, Configuration> cache;
@Override
public boolean isCustomTag(Named element, HtmlSource source) {
return getConfiguration(source).getTagsNames().contains(element.name().toString());
}
@Override
public boolean isCustomAttribute(Attribute attribute, HtmlSource source) {
return getConfiguration(source).getAttributesNames().contains(attribute.name().toString());
}
private Configuration getConfiguration(HtmlSource source) {
if (cache == null) {
//no cache - create
FileObject sourceFileObject = source.getSourceFileObject();
Project project = sourceFileObject == null ? null : FileOwnerQuery.getOwner(sourceFileObject);
Configuration conf = project == null ? Configuration.EMPTY : Configuration.get(project);
cache = Pair.of(source, conf);
return cache.second();
} else {
//check if the current source is the cached one
if (source == cache.first()) {
//yes, just return cached conf
return cache.second();
} else {
//no, reset cache and try again
cache = null;
return getConfiguration(source);
}
}
}
@Override
public void computeSuggestions(HintsProvider.HintsManager manager, RuleContext context, List<Hint> hints, int caretOffset) {
HtmlParserResult result = (HtmlParserResult) context.parserResult;
Node root = result.root(SyntaxAnalyzerResult.FILTERED_CODE_NAMESPACE);
Snapshot snapshot = result.getSnapshot();
int embeddedCaretOffset = snapshot.getEmbeddedOffset(caretOffset);
Element found = ElementUtils.findByPhysicalRange(root, embeddedCaretOffset, false);
if (found != null) {
switch (found.type()) {
case OPEN_TAG:
case CLOSE_TAG:
Named named = (Named) found;
String elementName = named.name().toString();
Configuration conf = Configuration.get(snapshot.getSource().getFileObject());
if (conf.getTagsNames().contains(elementName)) {
//custom element
hints.add(new CustomElementHint(elementName, context, new OffsetRange(snapshot.getOriginalOffset(found.from()), snapshot.getOriginalOffset(found.to()))));
}
//TODO add check + fix for missing required attributes
}
}
}
@Override
public void computeErrors(HintsProvider.HintsManager manager, final RuleContext context, final List<Hint> hints, List<Error> unhandled) {
HtmlParserResult result = (HtmlParserResult) context.parserResult;
Node root = result.root(SyntaxAnalyzerResult.FILTERED_CODE_NAMESPACE);
final Snapshot snapshot = result.getSnapshot();
final Configuration conf = Configuration.get(snapshot.getSource().getFileObject());
ElementUtils.visitChildren(root, CheckerElementVisitor.getChecker(context, conf, snapshot, hints));
}
@Override
public List<CompletionItem> completeOpenTags(CompletionContext context) {
List<CompletionItem> items = new ArrayList<>();
FileObject file = context.getResult().getSnapshot().getSource().getFileObject();
Configuration conf = Configuration.EMPTY;
if (file != null) {
conf = Configuration.get(file);
}
for (Tag t : conf.getTags()) {
String tagName = t.getName();
if (tagName.startsWith(context.getPrefix())) {
items.add(new CustomTagCompletionItem(t, context.getCCItemStartOffset()));
}
}
return items;
}
@Override
public List<CompletionItem> completeAttributes(CompletionContext context) {
Element node = context.getCurrentNode();
FileObject fileObject = context.getResult().getSnapshot().getSource().getFileObject();
if (node.type() != ElementType.OPEN_TAG || fileObject == null) {
return Collections.emptyList();
}
OpenTag ot = (OpenTag) node;
List<CompletionItem> items = new ArrayList<>();
Configuration conf = Configuration.get(fileObject);
String tagName = ((OpenTag) node).name().toString();
Tag t = conf.getTag(tagName);
if (t != null) {
//complete attribute specific for the element
for (org.netbeans.modules.html.custom.conf.Attribute a : t.getAttributes()) {
//do not complete already existing attributes
String aName = a.getName();
if (ot.getAttribute(aName) == null) {
if (aName.startsWith(context.getPrefix())) {
items.add(new CustomAttributeCompletionItem(a, context.getCCItemStartOffset()));
}
}
}
}
//complete global attributes
for (org.netbeans.modules.html.custom.conf.Attribute a : conf.getAttributes()) {
Collection<String> contexts = a.getContexts();
//complete either contextfree attribute or if the current element is the attribute context
if (contexts.isEmpty() || contexts.contains(tagName)) {
//do not complete already existing attributes
String aName = a.getName();
if (ot.getAttribute(aName) == null) {
if (aName.startsWith(context.getPrefix())) {
items.add(new CustomAttributeCompletionItem(a, context.getCCItemStartOffset()));
}
}
}
}
return items;
}
}