blob: e1e55392fca2a9848eff5feda61f00a9b3a790ff [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.css.prep.editor;
import java.io.IOException;
import org.netbeans.modules.css.prep.editor.model.CPModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.modules.csl.api.ColoringAttributes;
import org.netbeans.modules.csl.api.CompletionProposal;
import org.netbeans.modules.csl.api.DeclarationFinder;
import org.netbeans.modules.csl.api.DeclarationFinder.DeclarationLocation;
import org.netbeans.modules.csl.api.ElementHandle;
import org.netbeans.modules.csl.api.ElementKind;
import org.netbeans.modules.csl.api.HtmlFormatter;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.api.StructureItem;
import org.netbeans.modules.css.editor.module.spi.CompletionContext;
import org.netbeans.modules.css.editor.module.spi.CssEditorModule;
import org.netbeans.modules.css.editor.module.spi.EditorFeatureContext;
import org.netbeans.modules.css.editor.module.spi.FeatureContext;
import org.netbeans.modules.css.editor.module.spi.FutureParamTask;
import org.netbeans.modules.css.editor.module.spi.SemanticAnalyzer;
import org.netbeans.modules.css.editor.module.spi.Utilities;
import org.netbeans.modules.css.lib.api.CssParserResult;
import org.netbeans.modules.css.lib.api.CssTokenId;
import org.netbeans.modules.css.lib.api.Node;
import org.netbeans.modules.css.lib.api.NodeType;
import org.netbeans.modules.css.lib.api.NodeUtil;
import org.netbeans.modules.css.lib.api.NodeVisitor;
import org.netbeans.modules.css.prep.editor.model.CPElement;
import org.netbeans.modules.css.prep.editor.model.CPElementHandle;
import org.netbeans.modules.css.prep.editor.model.CPElementType;
import org.netbeans.modules.parsing.api.ParserManager;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.Parser;
import org.netbeans.modules.web.common.api.DependencyType;
import org.netbeans.modules.web.common.api.LexerUtils;
import org.netbeans.modules.web.common.api.Lines;
import org.netbeans.modules.web.common.api.WebUtils;
import org.netbeans.modules.web.common.spi.ProjectWebRootQuery;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.Pair;
import org.openide.util.lookup.ServiceProvider;
/**
* CSS preprocessor {@link CssEditorModule} implementation.
*
* TODO fix the instant rename and the mark occurrences - they are pretty naive
* - not scoped at all :-)
*
* @author marekfukala
*/
@ServiceProvider(service = CssEditorModule.class)
public class CPCssEditorModule extends CssEditorModule {
private final SemanticAnalyzer semanticAnalyzer = new CPSemanticAnalyzer();
private static Map<NodeType, ColoringAttributes> COLORINGS;
private static final Collection<String> PSEUDO_CLASSES = Arrays.asList(new String[]{
"extend" //NOI18N
});
@Override
public SemanticAnalyzer getSemanticAnalyzer() {
return semanticAnalyzer;
}
@Override
@SuppressWarnings("fallthrough")
public List<CompletionProposal> getCompletionProposals(final CompletionContext context) {
final List<CompletionProposal> proposals = new ArrayList<>();
CPModel model = CPModel.getModel(context.getParserResult());
if (model == null) {
return Collections.emptyList();
}
List<CompletionProposal> allVars = new ArrayList<>(getVariableCompletionProposals(context, model));
//errorneous source
TokenSequence<CssTokenId> ts = context.getTokenSequence();
Token<CssTokenId> token = ts.token();
if (token == null) {
return Collections.emptyList();
}
CssTokenId tid = token.id();
CharSequence ttext = token.text();
char first = ttext.charAt(0);
switch (tid) {
case ERROR:
switch (first) {
case '$':
//"$" as a prefix - user likely wants to type variable
//check context
if (NodeUtil.getAncestorByType(context.getActiveTokenNode(), NodeType.rule) != null
|| NodeUtil.getAncestorByType(context.getActiveTokenNode(), NodeType.cp_mixin_block) != null) {
//in declarations node -> offer all vars
return Utilities.filterCompletionProposals(allVars, context.getPrefix(), true);
}
break;
}
case AT_SIGN:
switch (first) {
case '@':
//may be:
//1. @-rule beginning
//2. less variable
//1.@-rule
proposals.addAll(Utilities.createRAWCompletionProposals(model.getDirectives(), ElementKind.KEYWORD, context.getAnchorOffset()));
//2.less variables
if (model.getPreprocessorType() == CPType.LESS) {
proposals.addAll(allVars);
}
return Utilities.filterCompletionProposals(proposals, context.getPrefix(), true);
}
break;
case SASS_VAR:
//sass variable: $v|
if (model.getPreprocessorType() == CPType.SCSS) {
return Utilities.filterCompletionProposals(allVars, context.getPrefix(), true);
}
case AT_IDENT:
//not complete keyword (complete keyword have their own token types,
//but no need to complete them except documentation completion request
List<CompletionProposal> props = Utilities.createRAWCompletionProposals(model.getDirectives(), ElementKind.KEYWORD, context.getAnchorOffset());
//less variable: @va|
if (model.getPreprocessorType() == CPType.LESS) {
return Utilities.filterCompletionProposals(allVars, context.getPrefix(), true);
}
return Utilities.filterCompletionProposals(props, context.getPrefix(), true);
}
Node activeNode = context.getActiveNode();
boolean isError = false;
//skip to first non error or recovery parent
while (activeNode.type() == NodeType.error || activeNode.type() == NodeType.recovery) {
isError = true;
activeNode = activeNode.parent();
}
// NodeUtil.dumpTree(context.getParseTreeRoot());
switch (activeNode.type()) {
case bodyItem:
case mediaBody:
case mediaBodyItem:
case cssClass:
switch (tid) {
case WS:
//in stylesheet main body: @include |
//check the previous token
if (ts.movePrevious()) {
Token<CssTokenId> previousToken = ts.token();
if (previousToken.id() == CssTokenId.SASS_INCLUDE) {
//add all mixins
proposals.addAll(getMixinsCompletionProposals(context, model));
}
}
break;
case IDENT:
if (LexerUtils.followsToken(ts, CssTokenId.SASS_INCLUDE, true, true, CssTokenId.WS) != null) {
//in stylesheet main body: @include mix|
//ok so the ident if preceeded by WS and then by SASS_INCLUDE token
proposals.addAll(getMixinsCompletionProposals(context, model));
} else if (LexerUtils.followsToken(ts, CssTokenId.DOT, true, true, CssTokenId.WS) != null) {
//in stylesheet main body: .mix| --> less mixins
proposals.addAll(getMixinsCompletionProposals(context, model));
}
break;
case DOT:
proposals.addAll(getMixinsCompletionProposals(context, model));
break;
}
break;
case cp_mixin_call:
//@include |
case cp_mixin_name:
//@include mymi|
proposals.addAll(getMixinsCompletionProposals(context, model));
break;
case cp_variable:
//already in the prefix
proposals.addAll(allVars);
break;
case propertyValue:
//just $ or @ prefix
if (context.getPrefix().length() == 1 && context.getPrefix().charAt(0) == model.getPreprocessorType().getVarPrefix()) {
proposals.addAll(allVars);
}
break;
case declaration:
switch (tid) {
case DOT:
//div { .| } -- less mixin call
proposals.addAll(getMixinsCompletionProposals(context, model));
break;
case WS:
//go back and find first non white token
while (ts.movePrevious()
&& (ts.token().id() == CssTokenId.WS
|| ts.token().id() == CssTokenId.NL)) {
//skip ws backward
}
switch (ts.token().id()) {
case SASS_INCLUDE:
//completion at: @include |
proposals.addAll(getMixinsCompletionProposals(context, model));
break;
}
}
break;
case selectorsGroup:
switch (ts.token().id()) {
case DOT:
//.| in body => less mixins
proposals.addAll(getMixinsCompletionProposals(context, model));
}
break;
}
return Utilities.filterCompletionProposals(proposals, context.getPrefix(), true);
}
private static Collection<CompletionProposal> getVariableCompletionProposals(final CompletionContext context, CPModel model) {
//filter the variable at the current location (being typed)
Collection<CompletionProposal> proposals = new LinkedHashSet<>();
for (CPElement var : model.getVariables(context.getCaretOffset())) {
if (var.getType() != CPElementType.VARIABLE_USAGE && !var.getRange().containsInclusive(context.getCaretOffset())) {
ElementHandle handle = new CPCslElementHandle(context.getFileObject(), var.getName());
VariableCompletionItem item = new VariableCompletionItem(
handle,
var.getHandle(),
context.getAnchorOffset(),
null); //no origin for current file
// var.getFile() == null ? null : var.getFile().getNameExt());
proposals.add(item);
}
}
try {
//now gather global vars from all linked sheets
FileObject file = context.getFileObject();
if (file != null) {
Map<FileObject, CPCssIndexModel> indexModels = CPUtils.getIndexModels(file, DependencyType.REFERRING_AND_REFERRED, true);
for (Entry<FileObject, CPCssIndexModel> entry : indexModels.entrySet()) {
FileObject reff = entry.getKey();
CPCssIndexModel cpIndexModel = entry.getValue();
Collection<org.netbeans.modules.css.prep.editor.model.CPElementHandle> variables = cpIndexModel.getVariables();
for (org.netbeans.modules.css.prep.editor.model.CPElementHandle var : variables) {
if (var.getType() == CPElementType.VARIABLE_GLOBAL_DECLARATION) {
ElementHandle handle = new CPCslElementHandle(context.getFileObject(), var.getName());
VariableCompletionItem item = new VariableCompletionItem(
handle,
var,
context.getAnchorOffset(),
reff.getNameExt());
proposals.add(item);
}
}
}
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return proposals;
}
private static Collection<CompletionProposal> getMixinsCompletionProposals(final CompletionContext context, CPModel model) {
//filter the variable at the current location (being typed)
Collection<CompletionProposal> proposals = new LinkedHashSet<>();
for (CPElement mixin : model.getMixins()) {
if (mixin.getType() == CPElementType.MIXIN_DECLARATION) {
ElementHandle handle = new CPCslElementHandle(context.getFileObject(), mixin.getName());
MixinCompletionItem item = new MixinCompletionItem(
handle,
mixin.getHandle(),
context.getAnchorOffset(),
null); //no origin for current file
// var.getFile() == null ? null : var.getFile().getNameExt());
proposals.add(item);
}
}
try {
//now gather global vars from all linked sheets
FileObject file = context.getFileObject();
if (file != null) {
Map<FileObject, CPCssIndexModel> indexModels = CPUtils.getIndexModels(file, DependencyType.REFERRING_AND_REFERRED, true);
for (Entry<FileObject, CPCssIndexModel> entry : indexModels.entrySet()) {
FileObject reff = entry.getKey();
CPCssIndexModel cpIndexModel = entry.getValue();
Collection<org.netbeans.modules.css.prep.editor.model.CPElementHandle> mixins = cpIndexModel.getMixins();
for (org.netbeans.modules.css.prep.editor.model.CPElementHandle mixin : mixins) {
if (mixin.getType() == CPElementType.MIXIN_DECLARATION) {
ElementHandle handle = new CPCslElementHandle(context.getFileObject(), mixin.getName());
MixinCompletionItem item = new MixinCompletionItem(
handle,
mixin,
context.getAnchorOffset(),
reff.getNameExt());
proposals.add(item);
}
}
}
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return proposals;
}
@Override
public <T extends Map<OffsetRange, Set<ColoringAttributes>>> NodeVisitor<T> getSemanticHighlightingNodeVisitor(FeatureContext context, T result) {
final Snapshot snapshot = context.getSnapshot();
return new NodeVisitor<T>(result) {
@Override
public boolean visit(Node node) {
ColoringAttributes coloring = getColorings().get(node.type());
if (coloring != null) {
int dso = snapshot.getOriginalOffset(node.from());
int deo = snapshot.getOriginalOffset(node.to());
if (dso >= 0 && deo >= 0) { //filter virtual nodes
//check vendor speficic property
OffsetRange range = new OffsetRange(dso, deo);
getResult().put(range, Collections.singleton(coloring));
}
}
return false;
}
};
}
private static Map<NodeType, ColoringAttributes> getColorings() {
if (COLORINGS == null) {
COLORINGS = new EnumMap<>(NodeType.class);
COLORINGS.put(NodeType.cp_variable, ColoringAttributes.LOCAL_VARIABLE);
COLORINGS.put(NodeType.cp_mixin_name, ColoringAttributes.PRIVATE);
}
return COLORINGS;
}
@Override
public <T extends Set<OffsetRange>> NodeVisitor<T> getMarkOccurrencesNodeVisitor(EditorFeatureContext context, T result) {
return Utilities.createMarkOccurrencesNodeVisitor(context, result, NodeType.cp_variable, NodeType.cp_mixin_name);
}
@Override
public boolean isInstantRenameAllowed(EditorFeatureContext context) {
TokenSequence<CssTokenId> tokenSequence = context.getTokenSequence();
int diff = tokenSequence.move(context.getCaretOffset());
if (diff > 0 && tokenSequence.moveNext() || diff == 0 && tokenSequence.movePrevious()) {
Token<CssTokenId> token = tokenSequence.token();
return token.id() == CssTokenId.AT_IDENT //less
|| token.id() == CssTokenId.SASS_VAR //sass
|| token.id() == CssTokenId.IDENT; //sass/less mixin name
}
return false;
}
@Override
public <T extends Set<OffsetRange>> NodeVisitor<T> getInstantRenamerVisitor(EditorFeatureContext context, T result) {
TokenSequence<CssTokenId> tokenSequence = context.getTokenSequence();
int diff = tokenSequence.move(context.getCaretOffset());
if (diff > 0 && tokenSequence.moveNext() || diff == 0 && tokenSequence.movePrevious()) {
Token<CssTokenId> token = tokenSequence.token();
final CharSequence elementName = token.text();
return new NodeVisitor<T>(result) {
@Override
public boolean visit(Node node) {
switch (node.type()) {
case cp_mixin_name:
case cp_variable:
if (LexerUtils.equals(elementName, node.image(), false, false)) {
OffsetRange range = new OffsetRange(node.from(), node.to());
getResult().add(range);
break;
}
}
return false;
}
};
}
return null;
}
@Override
public Pair<OffsetRange, FutureParamTask<DeclarationLocation, EditorFeatureContext>> getDeclaration(Document document, int caretOffset) {
//first try to find the reference span
TokenSequence<CssTokenId> ts = LexerUtils.getJoinedTokenSequence(document, caretOffset, CssTokenId.language());
if (ts == null) {
return null;
}
OffsetRange foundRange = null;
Token<CssTokenId> token = ts.token();
int quotesDiff = WebUtils.isValueQuoted(ts.token().text().toString()) ? 1 : 0;
OffsetRange range = new OffsetRange(ts.offset() + quotesDiff, ts.offset() + ts.token().length() - quotesDiff);
CharSequence mixinName;
//MIXINs go to declaration
switch (token.id()) {
case IDENT:
mixinName = token.text();
//check if there is @import token before
while (ts.movePrevious() && ts.token().id() == CssTokenId.WS) {
}
Token t = ts.token();
if (t != null) {
if (t.id() == CssTokenId.DOT || t.id() == CssTokenId.SASS_INCLUDE) {
//gotcha!
//@import xxx --sass
//.xxx --less
foundRange = range;
}
}
if (foundRange == null) {
return null;
}
final CharSequence searchedMixinName = mixinName;
FutureParamTask<DeclarationLocation, EditorFeatureContext> callable = new FutureParamTask<DeclarationLocation, EditorFeatureContext>() {
@Override
public DeclarationLocation run(EditorFeatureContext context) {
final Collection<Pair<CPCslElementHandle, Snapshot>> locations = new ArrayList<>();
//first look at the current file
CPModel model = CPModel.getModel(context.getParserResult());
for (CPElement mixin : model.getMixins()) {
if (mixin.getType() == CPElementType.MIXIN_DECLARATION) {
if (LexerUtils.equals(searchedMixinName, mixin.getName(), false, false)) {
locations.add(
Pair.of(new CPCslElementHandle(
context.getFileObject(), mixin.getName(), mixin.getRange(), mixin.getType()),
context.getSnapshot()));
}
}
}
//then look at the referred files
try {
Map<FileObject, CPCssIndexModel> indexModels = CPUtils.getIndexModels(context.getFileObject(), DependencyType.REFERRING_AND_REFERRED, true);
for (Entry<FileObject, CPCssIndexModel> entry : indexModels.entrySet()) {
final CPCssIndexModel im = entry.getValue();
final FileObject file = entry.getKey();
Source source = Source.create(file);
ParserManager.parse(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
ResultIterator cssRI = WebUtils.getResultIterator(resultIterator, "text/css");
if (cssRI != null) {
Parser.Result parserResult = cssRI.getParserResult();
if (parserResult instanceof CssParserResult) {
CssParserResult result = (CssParserResult) parserResult;
CPModel model = CPModel.getModel(result);
for (CPElementHandle mixin : im.getMixins()) {
if (mixin.getType() == CPElementType.MIXIN_DECLARATION
&& LexerUtils.equals(searchedMixinName, mixin.getName(), false, false)) {
CPElement element = mixin.resolve(model);
if (element != null) {
locations.add(Pair.of(new CPCslElementHandle(
file, mixin.getName(), element.getRange(), mixin.getType()),
result.getSnapshot()));
}
}
}
}
}
}
});
}
} catch (ParseException | IOException ex) {
Exceptions.printStackTrace(ex);
}
if (locations.isEmpty()) {
return DeclarationLocation.NONE;
} else {
Iterator<Pair<CPCslElementHandle, Snapshot>> itr = locations.iterator();
DeclarationLocation main = null;
while (itr.hasNext()) {
Pair<CPCslElementHandle, Snapshot> item = itr.next();
CPCslElementHandle handle = item.first();
Snapshot snapshot = item.second();
Lines lines = new Lines(snapshot.getText());
DeclarationLocation location = new DeclarationLocation(
handle.getFileObject(), handle.getOffsetRange(null).getStart());
if (main == null) {
main = location;
}
DeclarationFinder.AlternativeLocation alternative
= new CpAlternativeLocation(handle, location, snapshot, lines, handle.getFileObject().equals(context.getSource().getFileObject()));
main.addAlternative(alternative);
}
return main;
}
}
};
return Pair.<OffsetRange, FutureParamTask<DeclarationLocation, EditorFeatureContext>>of(foundRange, callable);
case SASS_VAR:
case AT_IDENT: //less var //TODO - add default directives - see the css grammar file comment about that
//cp variable
final String varName = token.text().toString();
foundRange = new OffsetRange(ts.offset(), ts.offset() + ts.token().length());
callable = new FutureParamTask<DeclarationLocation, EditorFeatureContext>() {
@Override
public DeclarationLocation run(EditorFeatureContext context) {
final Collection<Pair<CPCslElementHandle, Snapshot>> locations = new ArrayList<>();
//first look at the current file
CPModel model = CPModel.getModel(context.getParserResult());
for (CPElement var : model.getVariables()) {
if (var.getType().isOfTypes(CPElementType.VARIABLE_GLOBAL_DECLARATION, CPElementType.VARIABLE_LOCAL_DECLARATION, CPElementType.VARIABLE_DECLARATION_IN_BLOCK_CONTROL)) {
if (LexerUtils.equals(varName, var.getName(), false, false)) {
locations.add(
Pair.of(
new CPCslElementHandle(
context.getFileObject(),
var.getName(),
var.getRange(),
var.getType()),
context.getSnapshot()));
}
}
}
try {
//then look at the referred files
Map<FileObject, CPCssIndexModel> indexModels = CPUtils.getIndexModels(context.getFileObject(), DependencyType.REFERRING_AND_REFERRED, true);
for (Entry<FileObject, CPCssIndexModel> entry : indexModels.entrySet()) {
final CPCssIndexModel im = entry.getValue();
final FileObject file = entry.getKey();
Source source = Source.create(file);
ParserManager.parse(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
ResultIterator cssRI = WebUtils.getResultIterator(resultIterator, "text/css");
if (cssRI != null) {
CssParserResult result = (CssParserResult) cssRI.getParserResult();
CPModel model = CPModel.getModel(result);
for (CPElementHandle var : im.getVariables()) {
if (var.getType() == CPElementType.VARIABLE_GLOBAL_DECLARATION && var.getName().equals(varName)) {
CPElement element = var.resolve(CPModel.getModel(file));
if (element != null) {
locations.add(
Pair.of(
new CPCslElementHandle(
file,
var.getName(),
element.getRange(),
var.getType()),
result.getSnapshot()
));
}
}
}
}
}
});
}
} catch (ParseException | IOException ex) {
Exceptions.printStackTrace(ex);
}
if (locations.isEmpty()) {
return DeclarationLocation.NONE;
} else {
Iterator<Pair<CPCslElementHandle, Snapshot>> itr = locations.iterator();
DeclarationLocation main = null;
while (itr.hasNext()) {
Pair<CPCslElementHandle, Snapshot> item = itr.next();
CPCslElementHandle handle = item.first();
Snapshot snapshot = item.second();
Lines lines = new Lines(snapshot.getText());
DeclarationLocation location = new DeclarationLocation(
handle.getFileObject(), handle.getOffsetRange(null).getStart());
if (main == null) {
main = location;
}
DeclarationFinder.AlternativeLocation alternative
= new CpAlternativeLocation(handle, location, snapshot, lines, handle.getFileObject().equals(context.getSource().getFileObject()));
main.addAlternative(alternative);
}
return main;
}
}
};
return Pair.<OffsetRange, FutureParamTask<DeclarationLocation, EditorFeatureContext>>of(foundRange, callable);
default:
return null;
}
}
@Override
public <T extends Map<String, List<OffsetRange>>> NodeVisitor<T> getFoldsNodeVisitor(FeatureContext context, T result) {
final Snapshot snapshot = context.getSnapshot();
final Lines lines = new Lines(snapshot.getText());
return new NodeVisitor<T>(result) {
@Override
public boolean visit(Node node) {
switch (node.type()) {
case sass_control_block:
case cp_mixin_block:
case sass_map:
case sass_function_declaration:
//find the ruleSet curly brackets and create the fold between them inclusive
int from = node.from();
int to = node.to();
try {
//do not creare one line folds
if (lines.getLineIndex(from) < lines.getLineIndex(to)) {
List<OffsetRange> codeblocks = getResult().get("codeblocks"); //NOI18N
if (codeblocks == null) {
codeblocks = new ArrayList<>();
getResult().put("codeblocks", codeblocks); //NOI18N
}
codeblocks.add(new OffsetRange(from, to));
}
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
}
return false;
}
};
}
@Override
public <T extends List<StructureItem>> NodeVisitor<T> getStructureItemsNodeVisitor(FeatureContext context, T result) {
final Set<StructureItem> vars = new HashSet<>();
final Set<StructureItem> mixins = new HashSet<>();
CPModel model = CPModel.getModel(context.getParserResult());
for (CPElement element : model.getElements()) {
switch (element.getType()) {
case MIXIN_DECLARATION:
mixins.add(new CPStructureItem.Mixin(element));
break;
// case VARIABLE_DECLARATION_IN_BLOCK_CONTROL:
case VARIABLE_GLOBAL_DECLARATION:
// case VARIABLE_LOCAL_DECLARATION:
vars.add(new CPStructureItem.Variable(element));
break;
}
}
if (!vars.isEmpty()) {
result.add(new CPCategoryStructureItem.Variables(vars, context));
}
if (!mixins.isEmpty()) {
result.add(new CPCategoryStructureItem.Mixins(mixins, context));
}
//XXX ugly - we need no visitor, but still forced to return one
return new NodeVisitor<T>() {
@Override
public boolean visit(Node node) {
return true;
}
};
}
@Override
public Collection<String> getPseudoClasses(EditorFeatureContext context) {
Collection<String> result = null;
if (CPUtils.LESS_FILE_MIMETYPE.equals(context.getSource().getMimeType())) {
result = PSEUDO_CLASSES;
}
return result;
}
private static class CpAlternativeLocation implements DeclarationFinder.AlternativeLocation {
private static final int TEXT_MAX_LENGTH = 50;
private final CPCslElementHandle handle;
private final DeclarationLocation location;
private int lineIndex = -1;
private String lineText;
private final boolean currentFile;
public CpAlternativeLocation(CPCslElementHandle handle, DeclarationLocation location, Snapshot snapshot, Lines lines, boolean currentFile) {
this.handle = handle;
this.location = location;
this.currentFile = currentFile;
try {
lineIndex = lines.getLineIndex(location.getOffset());
//line bounds
int from = lines.getLineOffset(lineIndex);
int to = lines.getLinesCount() > (lineIndex + 1)
? lines.getLineOffset(lineIndex + 1)
: snapshot.getText().length();
lineText = snapshot.getText().subSequence(from, to).toString();
} catch (BadLocationException ex) {
Logger.getLogger(CpAlternativeLocation.class.getName()).log(Level.INFO, null, ex);
}
}
@Override
public ElementHandle getElement() {
return handle;
}
@Override
public String getDisplayHtml(HtmlFormatter b) {
//line text section
if (lineText != null) {
//split the text to three parts: the element text itself, its prefix and postfix
//then render the element test in bold
String elementText = handle.getName();
String prefix = "";
String postfix = "";
//strip the line to the body start
int elementIndex = lineText.indexOf(elementText);
if (elementIndex >= 0) {
//find the closest opening curly bracket or NL forward
int to;
for (to = elementIndex; to < lineText.length(); to++) {
char c = lineText.charAt(to);
if (c == '{' || c == '\n') {
break;
}
}
//now find nearest closing curly bracket or newline backward
int from;
for (from = elementIndex; from >= 0; from--) {
char ch = lineText.charAt(from);
if (ch == '}' || ch == '\n') {
break;
}
}
prefix = lineText.substring(from + 1, elementIndex).trim();
postfix = lineText.substring(elementIndex + elementText.length(), to).trim();
//now strip the prefix and postfix so the whole text is not longer than SELECTOR_TEXT_MAX_LENGTH
int overlap = (prefix.length() + elementText.length() + postfix.length()) - TEXT_MAX_LENGTH;
if (overlap > 0) {
//strip
int stripFromPrefix = Math.min(overlap / 2, prefix.length());
prefix = ".." + prefix.substring(stripFromPrefix);
int stripFromPostfix = Math.min(overlap - stripFromPrefix, postfix.length());
postfix = postfix.substring(0, postfix.length() - stripFromPostfix) + "..";
}
}
b.appendHtml("<span>");//NOI18N
b.appendText(prefix);
b.appendText(" "); //NOI18N
b.appendHtml("<b>"); //NOI18N
b.appendText(elementText);
b.appendHtml("</b>"); //NOI18N
b.appendText(" "); //NOI18N
b.appendText(postfix);
b.appendHtml("</span>"); //NOI18N
}
//file:offset section
b.appendHtml("<span>");
if (!isLocalDeclaration()) {
//add a link to the file relative to the web root
FileObject file = location.getFileObject();
FileObject pathRoot = ProjectWebRootQuery.getWebRoot(file);
String path = null;
String resolveTo = null;
if (pathRoot != null) {
path = FileUtil.getRelativePath(pathRoot, file); //this may also return null
}
if (path == null) {
//the file cannot be resolved relatively to the webroot or no webroot found
//try to resolve relative path to the project's root folder
Project project = FileOwnerQuery.getOwner(file);
if (project != null) {
pathRoot = project.getProjectDirectory();
path = FileUtil.getRelativePath(pathRoot, file); //this may also return null
if (path != null) {
resolveTo = "${project.home}/"; //NOI18N
}
}
}
if (path == null) {
//if everything fails, just use the absolute path
path = file.getPath();
}
b.appendText(" in ");
if (resolveTo != null) {
b.appendHtml("<i>"); //NOI18N
b.appendText(resolveTo);
b.appendHtml("</i>"); //NOI18N
}
b.appendText(path);
if (lineIndex != -1) {
b.appendText(":"); //NOI18N
b.appendText(Integer.toString(lineIndex + 1)); //line offsets are counted from zero, but in editor lines starts with one.
}
} else {
b.appendText(" at line ");
b.appendText(Integer.toString((lineIndex + 1)));
}
b.appendHtml("</span>");
return b.getText();
}
@Override
public DeclarationLocation getLocation() {
return location;
}
@Override
public int compareTo(DeclarationFinder.AlternativeLocation o) {
CpAlternativeLocation cpal = (CpAlternativeLocation) o;
if (isLocalDeclaration() == cpal.isLocalDeclaration()) {
return location.getFileObject().getPath().compareTo(o.getLocation().getFileObject().getPath());
} else {
return isLocalDeclaration() && !cpal.isLocalDeclaration() ? -1 : +1;
}
}
/**
* Is the declaration location in the current file?
*/
private boolean isLocalDeclaration() {
return currentFile;
}
}
}