/* * Copyright 2010 Ronnie Kolehmainen * * Licensed 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 com.github.cssxfire; import com.github.cssxfire.tree.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Ref; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiFile; import com.intellij.psi.css.*; import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.PsiElementProcessor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; /** * Created by IntelliJ IDEA. * User: Ronnie */ public class IncomingChangesProcessor { private static final Logger LOG = Logger.getInstance(IncomingChangesProcessor.class.getName()); private final Project project; private final FirebugChangesBean changesBean; private IncomingChangesProcessor(Project project, FirebugChangesBean changesBean) { this.project = project; this.changesBean = changesBean; } /** * Process files and CSS elements within the project according to the information reported by the Firebug extension. * The mission is to find all possible code that could be affected by the property change in Firebug CSS editor. * * @param project the project * @param changesBean the changes picked up from the Firebug extension * @return all candidates matching the selector, media query, and filename contained in the bean */ static Collection<CssDeclarationPath> getProjectCandidates(Project project, FirebugChangesBean changesBean) { return new IncomingChangesProcessor(project, changesBean).getCandidates(); } private Collection<CssDeclarationPath> getCandidates() { final List<CssDeclarationPath> candidates = new ArrayList<CssDeclarationPath>(); // find possible media query targets with its own processor Set<CssMediumList> mediaCandidates = findCandidateMediaLists(); // find possible file targets with its own search Set<PsiFile> fileCandidates = findCandidateFiles(); // search for existing selectors CssSelectorSearchProcessor selectorProcessor = SearchProcessorCache.getInstance(project).getSelectorSearchProcessor(changesBean.getSelector()); CssBlock[] cssBlocks = selectorProcessor.getBlocks(); if (LOG.isDebugEnabled()) { LOG.debug("Searched CSS selectors for '" + selectorProcessor.getSearchWord() + "' ('" + selectorProcessor.getSelector() + "'), got " + cssBlocks.length + " results"); } for (CssBlock block : cssBlocks) { final Ref<CssDeclaration> destination = new Ref<CssDeclaration>(); CssUtils.processCssDeclarations(block, new PsiElementProcessor<CssDeclaration>() { public boolean execute(@NotNull CssDeclaration declaration) { if (changesBean.getProperty().equals(declaration.getPropertyName())) { destination.set(declaration); return false; } return true; } }); CssDeclaration existingDeclaration = destination.get(); PsiFile file = block.getContainingFile().getOriginalFile(); CssDeclarationPath cssDeclarationPath; if (existingDeclaration != null) { // found existing declaration, possibly by resolving mixin cssDeclarationPath = createPath(existingDeclaration, block); } else { // non-existing - create new cssDeclarationPath = createNewPath(file, block); } if (cssDeclarationPath != null) { candidates.add(cssDeclarationPath); } // remove from collected files and media deleteCandidate(fileCandidates, file); deleteCandidate(mediaCandidates, CssUtils.findMediumList(block)); } // add candidates from remaining media candidates for (CssMediumList mediaCandidate : mediaCandidates) { // remove from collected files PsiFile file = mediaCandidate.getContainingFile().getOriginalFile(); deleteCandidate(fileCandidates, file); CssDeclarationPath cssDeclarationPath = createNewPath(file, mediaCandidate); if (cssDeclarationPath != null) { candidates.add(cssDeclarationPath); } } // add candidate paths for remaining files for (PsiFile fileCandidate : fileCandidates) { CssRulesetList rulesetList = CssUtils.findFirstCssRulesetList(fileCandidate); if (rulesetList != null) { CssDeclarationPath cssDeclarationPath = createNewPath(fileCandidate, rulesetList); if (cssDeclarationPath != null) { candidates.add(cssDeclarationPath); } } } return candidates; } /** * Assembles a path for a given CSS declaration and block. * * @param declaration the declaration * @param block the block * @return a path for an existing CSS declaration, or <tt>null</tt> if the containing file or directory can not be determined */ @Nullable private CssDeclarationPath createPath(CssDeclaration declaration, CssBlock block) { CssDeclarationNode declarationNode = new CssDeclarationNode(declaration, changesBean.getValue(), changesBean.isDeleted(), changesBean.isImportant()); CssSelectorNode selectorNode = new CssSelectorNode(changesBean.getSelector(), block); PsiFile file = declaration.getContainingFile().getOriginalFile(); if (!file.isValid()) { return null; } CssFileNode fileNode = new CssFileNode(file); PsiDirectory directory = file.getParent(); if (directory == null || !directory.isValid()) { LOG.warn("Invalid directory for existing path: " + (directory == null ? directory : directory.getVirtualFile().getUrl())); return null; } return new CssDeclarationPath(new CssDirectoryNode(directory), fileNode, selectorNode, declarationNode); } /** * Assembles a declaration path for a file and destination (anchor) psi element * * @param file the file * @param destinationElement the anchor * @return a path for a non-existing CSS declaration, or <tt>null</tt> if the containing file or directory can not be determined */ @Nullable private CssDeclarationPath createNewPath(PsiFile file, CssElement destinationElement) { if (!file.isValid()) { return null; } CssDeclaration declaration = CssUtils.createDeclaration(project, changesBean.getSelector(), changesBean.getProperty(), changesBean.getValue(), changesBean.isImportant()); if (declaration.getValue() == null) { LOG.warn("Unable to crteate PSI from " + changesBean); return null; } CssDeclarationNode declarationNode = CssNewDeclarationNode.forDestination(declaration, destinationElement, changesBean.isDeleted()); CssSelectorNode selectorNode = new CssSelectorNode(changesBean.getSelector(), destinationElement); CssFileNode fileNode = new CssFileNode(file); PsiDirectory directory = file.getParent(); if (directory == null || !directory.isValid()) { LOG.warn("Invalid directory for new path: " + (directory == null ? directory : directory.getVirtualFile().getUrl())); return null; } return new CssDeclarationPath(new CssDirectoryNode(directory), fileNode, selectorNode, declarationNode); } /** * Searches the project for all medium lists with same query text as reported by the bean * * @return all matching media query elements */ @NotNull private Set<CssMediumList> findCandidateMediaLists() { final Set<CssMediumList> elements = new HashSet<CssMediumList>(); if (changesBean.getMedia().length() > 0) { CssMediaSearchProcessor mediaProcessor = SearchProcessorCache.getInstance(project).getMediaSearchProcessor(changesBean.getMedia()); Set<CssMediumList> mediaLists = mediaProcessor.getMediaLists(); if (LOG.isDebugEnabled()) { LOG.debug("Searched CSS media for " + mediaProcessor.getSearchWord() + ", got " + mediaLists.size() + " results"); } elements.addAll(mediaLists); } return elements; } /** * Searches the project for files with same name as reported by the bean * * @return all files matching the filename */ @NotNull private Set<PsiFile> findCandidateFiles() { final Set<PsiFile> files = new HashSet<PsiFile>(); if (changesBean.getFilename().length() > 0) { files.addAll(Arrays.asList(FilenameIndex.getFilesByName(project, changesBean.getFilename(), GlobalSearchScope.projectScope(project)))); } return files; } /** * Deletes <i>object</i> from the <i>collection</i>, if <i>object</i> is not <tt>null</tt> and contained by <i>collection</i> * * @param collection the collection * @param object the object to remove * @param <T> the type of collection */ private <T> void deleteCandidate(@NotNull Collection<T> collection, @Nullable T object) { if (object != null) { collection.remove(object); } } }