| /* |
| * 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.editor.indent; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import javax.swing.text.AbstractDocument; |
| import javax.swing.text.BadLocationException; |
| import javax.swing.text.Document; |
| import javax.swing.text.Position; |
| import org.netbeans.api.editor.mimelookup.MimeLookup; |
| import org.netbeans.api.editor.mimelookup.MimePath; |
| import org.netbeans.api.lexer.LanguagePath; |
| import org.netbeans.api.lexer.TokenHierarchy; |
| import org.netbeans.api.lexer.TokenSequence; |
| import org.netbeans.lib.editor.util.swing.MutablePositionRegion; |
| import org.netbeans.modules.editor.indent.spi.Context; |
| import org.netbeans.modules.editor.indent.spi.ExtraLock; |
| import org.netbeans.modules.editor.indent.spi.IndentTask; |
| import org.netbeans.modules.editor.indent.spi.ReformatTask; |
| import org.openide.util.Exceptions; |
| import org.openide.util.Lookup; |
| import org.openide.util.lookup.ProxyLookup; |
| |
| /** |
| * Indentation and code reformatting services for a swing text document. |
| * |
| * @author Miloslav Metelka |
| */ |
| public final class TaskHandler { |
| |
| // -J-Dorg.netbeans.modules.editor.indent.TaskHandler.level=FINE |
| private static final Logger LOG = Logger.getLogger(TaskHandler.class.getName()); |
| |
| private final boolean indent; |
| |
| private final Document doc; |
| |
| private List<MimeItem> items; |
| |
| /** |
| * Start position of the currently formatted chunk. |
| */ |
| private Position startPos; |
| |
| /** |
| * End position of the currently formatted chunk. |
| */ |
| private Position endPos; |
| |
| private Position caretPos; |
| |
| private final Set<Object> existingFactories = new HashSet<Object>(); |
| |
| private Lookup lookup = null; |
| |
| |
| TaskHandler(boolean indent, Document doc) { |
| this.indent = indent; |
| this.doc = doc; |
| } |
| |
| public Lookup getLookup() { |
| return lookup; |
| } |
| |
| public boolean isIndent() { |
| return indent; |
| } |
| |
| public Document document() { |
| return doc; |
| } |
| |
| public int caretOffset() { |
| return caretPos.getOffset(); |
| } |
| |
| public void setCaretOffset(int offset) throws BadLocationException { |
| caretPos = doc.createPosition(offset); |
| } |
| |
| public Position startPos() { |
| return startPos; |
| } |
| |
| public Position endPos() { |
| return endPos; |
| } |
| |
| void setGlobalBounds(Position startPos, Position endPos) { |
| assert (startPos.getOffset() <= endPos.getOffset()) |
| : "startPos=" + startPos.getOffset() + " < endPos=" + endPos.getOffset(); |
| this.startPos = startPos; |
| this.endPos = endPos; |
| } |
| |
| boolean collectTasks() { |
| TokenHierarchy<?> th = TokenHierarchy.get(document()); |
| Set<LanguagePath> languagePathSet = Collections.emptySet(); |
| if (doc instanceof AbstractDocument) { |
| AbstractDocument adoc = (AbstractDocument)doc; |
| adoc.readLock(); |
| try { |
| languagePathSet = th.languagePaths(); |
| List<LanguagePath> languagePaths = new ArrayList<LanguagePath>(languagePathSet); |
| Collections.sort(languagePaths, LanguagePathSizeComparator.ASCENDING); |
| for (LanguagePath lp : languagePaths) { |
| addItem(MimePath.parse(lp.mimePath()), lp); |
| } |
| } finally { |
| adoc.readUnlock(); |
| } |
| } |
| |
| if (languagePathSet.isEmpty()) { |
| addItem(MimePath.parse(docMimeType()), null); |
| } |
| |
| // XXX: HACK TODO PENDING WORKAROUND |
| // Temporary Workaround: the HTML formatter clobbers the Ruby formatter's |
| // work so make sure the Ruby formatter gets to work last in RHTML files |
| // |
| // The problem is that both html and ruby formatters have language paths |
| // of the same lenght and therefore their ordering is undefined. |
| // This will be solved in the infrastructure by segmenting the formatted |
| // area by the language paths. And calling each formatter task only |
| // with the segments that belong to it. |
| if (items != null && "application/x-httpd-eruby".equals(docMimeType())) { //NOI18N |
| // Copy list, except for Ruby element, which we then add at the end |
| List<MimeItem> newItems = new ArrayList<MimeItem>(items.size()); |
| MimeItem rubyItem = null; |
| for (MimeItem item : items) { |
| if (item.mimePath().getPath().endsWith("text/x-ruby")) { // NOI18N |
| rubyItem = item; |
| } else { |
| newItems.add(item); |
| } |
| } |
| if (rubyItem != null) { |
| newItems.add(rubyItem); |
| } |
| items = newItems; |
| } |
| |
| // current PHP formatter must run after HTML formatter |
| if (items != null && "text/x-php5".equals(docMimeType())) { //NOI18N |
| // Copy list, except for Ruby element, which we then add at the end |
| List<MimeItem> newItems = new ArrayList<MimeItem>(items.size()); |
| MimeItem phpItem = null; |
| for (MimeItem item : items) { |
| if (item.mimePath().getPath().endsWith("text/x-php5")) { // NOI18N |
| phpItem = item; |
| } else { |
| newItems.add(item); |
| } |
| } |
| if (phpItem != null) { |
| newItems.add(phpItem); |
| } |
| items = newItems; |
| } |
| |
| // XXX : Hack to get javascript formatter running as last one |
| if (items != null && items.size() > 1 && "text/javascript".equals(docMimeType())) { //NOI18N |
| // Copy list, except for JS element, which we then add at the end |
| List<MimeItem> newItems = new ArrayList<MimeItem>(items.size()); |
| MimeItem jsItem = null; |
| for (MimeItem item : items) { |
| if (item.mimePath().getPath().endsWith("text/javascript")) { // NOI18N |
| jsItem = item; |
| } else { |
| newItems.add(item); |
| } |
| } |
| if (jsItem != null) { |
| newItems.add(jsItem); |
| } |
| items = newItems; |
| } |
| |
| // XXX: HACK TODO PENDING WORKAROUND |
| // A hotfix for #116022: the jsp formatter must be called first and the html formatter second |
| if (items != null && "text/x-jsp".equals(docMimeType()) || "text/x-tag".equals(docMimeType())) { //NOI18N |
| List<MimeItem> newItems = new ArrayList<MimeItem>(items.size()); |
| MimeItem htmlItem = null; |
| MimeItem jspItem = null; |
| for (MimeItem item : items) { |
| if (item.mimePath().getPath().endsWith("text/html")) { //NOI18N |
| htmlItem = item; |
| } else if (item.mimePath().getPath().endsWith("text/x-jsp") //NOI18N |
| || item.mimePath().getPath().endsWith("text/x-tag")) { //NOI18N |
| jspItem = item; |
| } else { |
| newItems.add(item); |
| } |
| } |
| |
| if (htmlItem != null) { |
| newItems.add(0, htmlItem); |
| } |
| |
| if (jspItem != null) { |
| newItems.add(0, jspItem); |
| } |
| |
| items = newItems; |
| } |
| |
| if (items != null) { |
| List<Lookup> lookups = new ArrayList<Lookup>(); |
| for (MimeItem mi : items) { |
| Lookup l = mi.getLookup(); |
| if (l != null) { |
| lookups.add(l); |
| } |
| } |
| if (lookups.size() > 0) { |
| lookup = new ProxyLookup(lookups.toArray(new Lookup[lookups.size()])); |
| } |
| } |
| |
| if (lookup == null) { |
| lookup = Lookup.EMPTY; |
| } |
| |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("Collected items: "); //NOI18N |
| if (items != null) { |
| for (MimeItem mi : items) { |
| LOG.fine(" Item: " + mi); //NOI18N |
| } |
| } |
| LOG.fine("-----------------"); //NOI18N |
| } |
| |
| return (items != null); |
| } |
| |
| void lock() { |
| if (items != null) { |
| int i = 0; |
| try { |
| for (; i < items.size(); i++) { |
| MimeItem item = items.get(i); |
| item.lock(); |
| } |
| } finally { |
| if (i < items.size()) { // Locking of i-th item has failed |
| // Unlock the <0,i-1> items that are already locked |
| // Assuming that the unlock() for already locked items will pass |
| while (--i >= 0) { |
| MimeItem item = items.get(i); |
| item.unlock(); |
| } |
| } |
| } |
| } |
| } |
| |
| void unlock() { |
| if (items != null) { |
| for (MimeItem item : items) { |
| item.unlock(); |
| } |
| } |
| } |
| |
| boolean hasFactories() { |
| String mimeType = docMimeType(); |
| return (mimeType != null && new MimeItem(this, MimePath.get(mimeType), null).hasFactories()); |
| } |
| |
| boolean hasItems() { |
| return (items != null); |
| } |
| |
| void runTasks() throws BadLocationException { |
| // Run top-level task and possibly embedded tasks according to the context |
| if (items == null) // Do nothing for no items |
| return; |
| |
| // Start with the doc's mime type's task |
| for (MimeItem item : items) { |
| item.runTask(); |
| } |
| } |
| |
| private boolean addItem(MimePath mimePath, LanguagePath languagePath) { |
| MimeItem item = new MimeItem(this, mimePath, languagePath); |
| if (item.createTask(existingFactories)) { |
| if (items == null) { |
| items = new ArrayList<MimeItem>(); |
| } |
| items.add(item); |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("Adding MimeItem: " + item); //NOI18N |
| } |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * Collect language paths used within the given token sequence |
| * |
| * @param ts non-null token sequence (or subsequence). <code>ts.moveNext()</code> |
| * is called first on it. |
| * @return collection of language paths present in the given token sequence. |
| */ |
| private Collection<LanguagePath> getActiveEmbeddedPaths(TokenSequence ts) { |
| Collection<LanguagePath> lps = new HashSet<LanguagePath>(); |
| lps.add(ts.languagePath()); |
| List<TokenSequence<?>> tsStack = null; |
| while (true) { |
| while (ts.moveNext()) { |
| TokenSequence<?> eTS = ts.embedded(); |
| if (eTS != null) { |
| tsStack.add(ts); |
| ts = eTS; |
| lps.add(ts.languagePath()); |
| } |
| } |
| if (tsStack != null && tsStack.size() > 0) { |
| ts = tsStack.get(tsStack.size() - 1); |
| tsStack.remove(tsStack.size() - 1); |
| } else { |
| break; |
| } |
| } |
| return lps; |
| } |
| |
| private String docMimeType() { |
| return (String)document().getProperty("mimeType"); //NOI18N |
| } |
| |
| /** |
| * Item that services indentation/reformatting for a single mime-path. |
| */ |
| public static final class MimeItem { |
| |
| private final TaskHandler handler; |
| |
| private final MimePath mimePath; |
| |
| private final LanguagePath languagePath; |
| |
| private IndentTask indentTask; |
| |
| private ReformatTask reformatTask; |
| |
| private ExtraLock extraLock; |
| |
| private Context context; |
| |
| MimeItem(TaskHandler handler, MimePath mimePath, LanguagePath languagePath) { |
| this.handler = handler; |
| this.mimePath = mimePath; |
| this.languagePath = languagePath; |
| } |
| |
| public MimePath mimePath() { |
| return mimePath; |
| } |
| |
| public LanguagePath languagePath() { |
| return languagePath; |
| } |
| |
| public Context context() { |
| if (context == null) { |
| context = IndentSpiPackageAccessor.get().createContext(this); |
| } |
| return context; |
| } |
| |
| public TaskHandler handler() { |
| return handler; |
| } |
| |
| boolean hasFactories() { |
| Lookup lookup = MimeLookup.getLookup(mimePath); |
| return handler().isIndent() |
| ? (lookup.lookup(IndentTask.Factory.class) != null) |
| : (lookup.lookup(ReformatTask.Factory.class) != null); |
| } |
| |
| public List<Context.Region> indentRegions() { |
| Document doc = handler.document(); |
| List<Context.Region> indentRegions = new ArrayList<Context.Region>(); |
| AbstractDocument adoc = null; |
| if (doc instanceof AbstractDocument) { |
| adoc = (AbstractDocument) doc; |
| adoc.readLock(); |
| } |
| try { |
| int startOffset = handler.startPos().getOffset(); |
| int endOffset = handler.endPos().getOffset(); |
| if (endOffset > doc.getLength()) |
| endOffset = Integer.MAX_VALUE; |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("indentRegions: startOffset=" + startOffset + ", endOffset=" + endOffset + '\n'); //NOI18N |
| } |
| if (languagePath != null && startOffset < endOffset) { |
| List<TokenSequence<?>> tsl = TokenHierarchy.get(doc).tokenSequenceList(languagePath, |
| startOffset, endOffset); |
| for (TokenSequence<?> ts : tsl) { |
| ts.moveStart(); |
| if (ts.moveNext()) { // At least one token |
| int regionStartOffset = ts.offset(); |
| ts.moveEnd(); // At least one token exists |
| ts.movePrevious(); |
| int regionEndOffset = ts.offset() + ts.token().length(); |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine(" Region[" + indentRegions.size() + // NOI18N |
| "]: startOffset=" + regionStartOffset + ", endOffset=" + regionEndOffset + '\n'); //NOI18N |
| } |
| // Only within global boundaries |
| if (regionStartOffset <= endOffset && regionEndOffset >= startOffset) { |
| regionStartOffset = Math.max(regionStartOffset, startOffset); |
| regionEndOffset = Math.min(regionEndOffset, endOffset); |
| MutablePositionRegion region = new MutablePositionRegion( |
| doc.createPosition(regionStartOffset), |
| doc.createPosition(regionEndOffset) |
| ); |
| indentRegions.add(IndentSpiPackageAccessor.get().createContextRegion(region)); |
| } |
| } |
| } |
| } else { // used when no token hierarchy exists |
| MutablePositionRegion wholeDocRegion = new MutablePositionRegion(handler.startPos, |
| handler.endPos); |
| indentRegions.add(IndentSpiPackageAccessor.get().createContextRegion(wholeDocRegion)); |
| } |
| |
| // Filter out guarded regions |
| // if (indentRegions.size() > 0 && doc instanceof GuardedDocument) { |
| // MutablePositionRegion region = IndentSpiPackageAccessor.get().positionRegion(indentRegions.get(0)); |
| // int regionStartOffset = region.getStartOffset(); |
| // GuardedDocument gdoc = (GuardedDocument)doc; |
| // int gbStartOffset = guardedBlocks.adjustToBlockEnd(region.getEndOffset()); |
| // MarkBlockChain guardedBlocks = gdoc.getGuardedBlockChain(); |
| // if (guardedBlocks != null && guardedBlocks.getChain() != null) { |
| // int gbStartOffset = guardedBlocks.adjustToNextBlockStart(indentRegions.getStartOffset()); |
| // int regionIndex = 0; |
| // while (regionIndex < indentRegions.size()) { // indentRegions can be mutated dynamically |
| // MutablePositionRegion region = IndentSpiPackageAccessor.get().positionRegion(indentRegions.get(regionIndex)); |
| // int gbStartOffset = guardedBlocks.adjustToNextBlockStart(region.getStartOffset()); |
| // int gbEndOffset = guardedBlocks.adjustToBlockEnd(region.getEndOffset()); |
| // |
| // while (pos < endPosition.getOffset()) { |
| // int stopPos = endPosition.getOffset(); |
| // if (gdoc != null) { // adjust to start of the next guarded block |
| // stopPos = gdoc.getGuardedBlockChain().adjustToNextBlockStart(pos); |
| // if (stopPos == -1 || stopPos > endPosition.getOffset()) { |
| // stopPos = endPosition.getOffset(); |
| // } |
| // } |
| // |
| // if (pos < stopPos) { |
| // int reformattedLen = formatter.reformat(doc, pos, stopPos); |
| // pos = pos + reformattedLen; |
| // } else { |
| // pos++; //ensure to make progress |
| // } |
| // |
| // if (gdoc != null) { // adjust to end of current block |
| // pos = gdoc.getGuardedBlockChain().adjustToBlockEnd(pos); |
| // } |
| // } |
| // } |
| // } |
| // } |
| } catch (BadLocationException e) { |
| Exceptions.printStackTrace(e); |
| indentRegions = Collections.emptyList(); |
| } finally { |
| if (adoc != null) { |
| adoc.readUnlock(); |
| } |
| } |
| return indentRegions; |
| } |
| |
| boolean createTask(Set<Object> existingFactories) { |
| Lookup lookup = MimeLookup.getLookup(mimePath); |
| if (!handler.isIndent()) { // Attempt reformat task first |
| ReformatTask.Factory factory = lookup.lookup(ReformatTask.Factory.class); |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("'" + mimePath.getPath() + "' supplied ReformatTask.Factory: " + factory); //NOI18N |
| } |
| if (factory != null && (reformatTask = factory.createTask(context())) != null |
| && !existingFactories.contains(factory)) { |
| extraLock = reformatTask.reformatLock(); |
| existingFactories.add(factory); |
| return true; |
| } |
| } |
| |
| if (handler.isIndent() || reformatTask == null) { // Possibly fallback to reindent for reformatting |
| IndentTask.Factory factory = lookup.lookup(IndentTask.Factory.class); |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("'" + mimePath.getPath() + "' supplied IndentTask.Factory: " + factory); //NOI18N |
| } |
| if (factory != null && (indentTask = factory.createTask(context())) != null |
| && !existingFactories.contains(factory)) { |
| extraLock = indentTask.indentLock(); |
| existingFactories.add(factory); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void lock() { |
| if (extraLock != null) |
| extraLock.lock(); |
| } |
| |
| void runTask() throws BadLocationException { |
| if (indentTask != null) { |
| indentTask.reindent(); |
| } else { |
| reformatTask.reformat(); |
| } |
| } |
| |
| void unlock() { |
| if (extraLock != null) |
| extraLock.unlock(); |
| } |
| |
| public @Override String toString() { |
| return mimePath + ": " + ((indentTask != null) ? "IT: " + indentTask : "RT: " + reformatTask); //NOI18N |
| } |
| |
| private Lookup getLookup() { |
| if (indentTask != null && indentTask instanceof Lookup.Provider) { |
| return ((Lookup.Provider)indentTask).getLookup(); |
| } else if (reformatTask != null && reformatTask instanceof Lookup.Provider) { |
| return ((Lookup.Provider)reformatTask).getLookup(); |
| } else { |
| return null; |
| } |
| } |
| } |
| |
| private static final class LanguagePathSizeComparator implements Comparator<LanguagePath> { |
| |
| static final LanguagePathSizeComparator ASCENDING = new LanguagePathSizeComparator(false); |
| |
| private final boolean reverse; |
| |
| public LanguagePathSizeComparator(boolean reverse) { |
| this.reverse = reverse; |
| } |
| |
| public int compare(LanguagePath lp1, LanguagePath lp2) { |
| return reverse ? lp2.size() - lp1.size() : lp1.size() - lp2.size(); |
| } |
| } // End of MimePathSizeComparator class |
| |
| } |