/******************************************************************************* * Copyright (c) 2005, 2017 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * *******************************************************************************/ package org.eclipse.dltk.tcl.internal.ui.text; import java.util.ArrayList; import java.util.List; import org.eclipse.dltk.ast.ASTNode; import org.eclipse.dltk.ast.ASTVisitor; import org.eclipse.dltk.ast.declarations.ModuleDeclaration; import org.eclipse.dltk.ast.expressions.StringLiteral; import org.eclipse.dltk.ast.parser.ISourceParser; import org.eclipse.dltk.ast.statements.Block; import org.eclipse.dltk.compiler.env.ModuleSource; import org.eclipse.dltk.core.DLTKCore; import org.eclipse.dltk.core.DLTKLanguageManager; import org.eclipse.dltk.tcl.ast.expressions.TclBlockExpression; import org.eclipse.dltk.tcl.ast.expressions.TclExecuteExpression; import org.eclipse.dltk.tcl.core.TclNature; import org.eclipse.dltk.tcl.core.ast.TclAdvancedExecuteExpression; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentExtension4; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.source.ICharacterPairMatcher; /** * Helper class for match pairs of characters. */ public final class TclPairMatcher implements ICharacterPairMatcher { private final boolean DEBUG = false; private static final int MAX_PARSE_WAIT_TIME = 100; private static final int MIN_PARSE_INTERVAL = 2500; private final Object lock = new Object(); private class ParserThread extends Thread { final String content; final long newTimestamp; final long newHashcode; final long startTime = System.currentTimeMillis(); /** * @param content * @param newTimestamp * @param newHashcode */ public ParserThread(String content, long newTimestamp, long newHashcode) { super(ParserThread.class.getName()); this.content = content; this.newTimestamp = newTimestamp; this.newHashcode = newHashcode; } @Override public void run() { try { if (DEBUG) { System.out.println("ParserThread - BEGIN"); //$NON-NLS-1$ } final PairBlock[] pairs = computePairRanges(content); synchronized (lock) { cachedPairs = pairs; cachedHash = newHashcode; cachedStamp = newTimestamp; parsedAt = startTime; } if (DEBUG) { System.out.println("ParserThread - END " //$NON-NLS-1$ + (System.currentTimeMillis() - startTime)); } } finally { synchronized (lock) { thread = null; } } } } private ParserThread thread = null; private long parsedAt = 0; private IDocument fDocument; private int fAnchor; private static class PairBlock { public PairBlock(int start, int end, char c) { this.start = start; this.end = end; } int start; int end; }; private PairBlock[] cachedPairs; private long cachedStamp = -1; private long cachedHash = Long.MAX_VALUE; public TclPairMatcher() { } private static PairBlock[] computePairRanges(final String contents) { /* * ISourceModule returned by editor.getInputModelElement() could be * inconsistent with current editor contents so we always reparse. */ final ISourceParser pp = DLTKLanguageManager .getSourceParser(TclNature.NATURE_ID); final ModuleDeclaration md = (ModuleDeclaration) pp .parse(new ModuleSource(contents), null); if (md == null) { return new PairBlock[0]; } final List<PairBlock> result = new ArrayList<>(); try { md.traverse(new ASTVisitor() { @Override public boolean visitGeneral(ASTNode be) throws Exception { if (be instanceof StringLiteral) { result.add(new PairBlock(be.sourceStart(), be.sourceEnd() - 1, '\"')); } else if (be instanceof TclExecuteExpression) { result.add(new PairBlock(be.sourceStart(), be.sourceEnd() - 1, '[')); } else if (be instanceof TclAdvancedExecuteExpression) { result.add(new PairBlock(be.sourceStart() - 1, be.sourceEnd(), '[')); } else if (be instanceof Block) { int start = be.sourceStart(); if (start != 0) { result.add(new PairBlock(start, be.sourceEnd() - 1, '{')); } } else if (be instanceof TclBlockExpression) { int start = be.sourceStart(); int end = be.sourceEnd(); if (start >= 0 && start < end && start < contents.length() && end <= contents.length() && contents.charAt(start) == '{' && contents.charAt(end - 1) == '}') { result.add(new PairBlock(start, end - 1, '{')); } } return super.visitGeneral(be); } }); } catch (Exception e) { if (DLTKCore.DEBUG) { e.printStackTrace(); } } return result.toArray(new PairBlock[result.size()]); } /** * Fully recalcs pairs for document * * @param doc * @throws BadLocationException */ private void recalc(final String content, long newTimestamp, long newHashcode) throws BadLocationException { final ParserThread t; synchronized (lock) { if (thread != null) { return; } thread = t = new ParserThread(content, newTimestamp, newHashcode); } t.start(); try { t.join(MAX_PARSE_WAIT_TIME); } catch (InterruptedException e) { // ignore } } /** * Recalcs pairs for the document, only if it is required */ private void updatePairs() throws BadLocationException { synchronized (lock) { if (System.currentTimeMillis() < parsedAt + MIN_PARSE_INTERVAL) { return; } } if (fDocument instanceof IDocumentExtension4) { final IDocumentExtension4 document = (IDocumentExtension4) fDocument; final long newTimestamp = document.getModificationStamp(); synchronized (lock) { if (newTimestamp == cachedStamp) { return; } } recalc(fDocument.get(), newTimestamp, Long.MAX_VALUE); } else { final String content = fDocument.get(); final int newHashCode = content.hashCode(); synchronized (lock) { if (newHashCode == cachedHash) { return; } } recalc(content, -1, newHashCode); } } private static boolean isBrace(char c) { return (c == '{' || c == '}' || c == '\"' || c == '[' || c == ']'); } /** * Tests that either the symbol at <code>offset</code> or the previous one * is a brace. This function checks that offsets are in the allowed range. * * @param document * @param offset * @return * @throws BadLocationException */ private static boolean isBraceAt(IDocument document, int offset) throws BadLocationException { // test symbol at offset if (offset < document.getLength() && isBrace(document.getChar(offset))) { return true; } // test previous symbol if (offset > 0 && isBrace(document.getChar(offset - 1))) { return true; } return false; } @Override public IRegion match(IDocument document, int offset) { if (document == null || offset < 0) { throw new IllegalArgumentException(); } try { fDocument = document; if (!isBraceAt(document, offset)) { return null; } updatePairs(); return matchPairsAt(offset); } catch (BadLocationException e) { if (DLTKCore.DEBUG_PARSER) e.printStackTrace(); } return null; } /* * (non-Javadoc) * * @see org.eclipse.jface.text.source.ICharacterPairMatcher#getAnchor() */ @Override public int getAnchor() { return fAnchor; } @Override public void dispose() { clear(); fDocument = null; } @Override public void clear() { } private IRegion matchPairsAt(int offset) { final PairBlock[] pairs; synchronized (lock) { pairs = cachedPairs; } if (pairs == null) { return null; } // TODO pairs should be sorted somehow... for (int i = 0, size = pairs.length; i < size; i++) { final PairBlock block = pairs[i]; if (offset == block.end + 1) { fAnchor = LEFT; return new Region(block.start, 1); } if (offset == block.start + 1) { fAnchor = LEFT; return new Region(block.end, 1); } } return null; } }