blob: 0e55b904d22abdb2fe1d75159908103c5ad4b7c4 [file] [log] [blame]
/*******************************************************************************
* 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;
}
}