| /* |
| * 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)); |
| } |
| 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; |
| } |
| } |
| |
| } |