// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.google.collide.client.documentparser;
import com.google.collide.client.util.BasicIncrementalScheduler;
import com.google.collide.client.util.IncrementalScheduler;
import com.google.collide.client.util.UserActivityManager;
import com.google.collide.codemirror2.Parser;
import com.google.collide.codemirror2.State;
import com.google.collide.codemirror2.SyntaxType;
import com.google.collide.codemirror2.Token;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.TaggableLine;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.Position;
import com.google.collide.shared.document.TextChange;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.anchor.AnchorManager;
import com.google.collide.shared.document.anchor.AnchorType;
import com.google.collide.shared.document.anchor.Anchor.RemovalStrategy;
import com.google.collide.shared.util.ListenerManager;
import com.google.collide.shared.util.ListenerRegistrar;
import com.google.collide.shared.util.ListenerManager.Dispatcher;
import com.google.collide.shared.util.ListenerRegistrar.Remover;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Parser for a document that delegates to CodeMirror.
*
* This class attaches to a document and re-parses whenever the contents
* changes. It uses an incremental parser allowing it to resume parsing from the
* beginning of the changed line.
*
*/
public class DocumentParser {
public static DocumentParser create(Document document, Parser codeMirrorParser,
UserActivityManager userActivityManager) {
/*
* Guess that parsing 300 lines takes 50ms, let scheduler balance actual
* parsing time per machine.
*/
BasicIncrementalScheduler scheduler = new BasicIncrementalScheduler(
userActivityManager, 50, 300);
return create(document, codeMirrorParser, scheduler);
}
@VisibleForTesting
public static DocumentParser create(Document document, Parser codeMirrorParser,
IncrementalScheduler scheduler) {
return new DocumentParser(document, codeMirrorParser, scheduler);
}
/**
* A listener that receives a callback as lines of the document get parsed.
* Can be called synchronously with user keyboard interactions
* or asynchronously in batch mode, parsing a few lines in a row.
*/
public interface Listener {
/**
* This method is called to mark the start of asynchronous parsing
* iteration.
*
* @param lineNumber number of a line iteration started from
*/
void onIterationStart(int lineNumber);
/**
* This method is called to mark the finish of asynchronous parsing
* iteration.
*/
void onIterationFinish();
/**
* Note: This may be called synchronously with a user's key press, so do not
* do too much work synchronously.
*/
void onDocumentLineParsed(Line line, int lineNumber, @Nonnull JsonArray<Token> tokens);
}
private static final AnchorType PARSER_ANCHOR_TYPE = AnchorType.create(DocumentParser.class,
"parser");
private Anchor createParserPosition(Document document) {
Anchor position =
document.getAnchorManager().createAnchor(PARSER_ANCHOR_TYPE, document.getFirstLine(), 0,
AnchorManager.IGNORE_COLUMN);
position.setRemovalStrategy(RemovalStrategy.SHIFT);
return position;
}
private final Parser codeMirrorParser;
private final Document.TextListener documentTextListener = new Document.TextListener() {
@Override
public void onTextChange(Document document, JsonArray<TextChange> textChanges) {
/*
* Tracks the earliest change in the document, so that can be used as a
* starting point for the parser
*/
Line earliestLine = parserPosition.getLine();
int earliestLineNumber = parserPosition.getLineNumber();
for (int i = 0, n = textChanges.size(); i < n; i++) {
TextChange textChange = textChanges.get(i);
Line line = textChange.getLine();
int lineNumber = textChange.getLineNumber();
if (lineNumber < earliestLineNumber) {
earliestLine = line;
earliestLineNumber = lineNumber;
}
// Synchronously parse this line
worker.parse(line, lineNumber, 1, null);
}
// Queue the earliest
document.getAnchorManager().moveAnchor(
parserPosition, earliestLine, earliestLineNumber, AnchorManager.IGNORE_COLUMN);
scheduler.schedule(parserTask);
}
};
private final ListenerManager<Listener> listenerManager;
private final Anchor parserPosition;
private final IncrementalScheduler.Task parserTask = new IncrementalScheduler.Task() {
@Override
public boolean run(int workAmount) {
return executeWorker(workAmount);
}
};
private final IncrementalScheduler scheduler;
private final DocumentParserWorker worker;
private final Remover textListenerRemover;
private DocumentParser(
Document document, Parser codeMirrorParser, IncrementalScheduler scheduler) {
Preconditions.checkNotNull(codeMirrorParser);
Preconditions.checkNotNull(scheduler);
this.codeMirrorParser = codeMirrorParser;
this.listenerManager = ListenerManager.create();
this.scheduler = scheduler;
this.worker = new DocumentParserWorker(this, codeMirrorParser);
this.parserPosition = createParserPosition(document);
this.textListenerRemover = document.getTextListenerRegistrar().add(documentTextListener);
}
/**
* Schedules the parsing of the document from the last parsed position, or the
* beginning of the document if this is the first time parsing.
*/
public void begin() {
scheduler.schedule(parserTask);
}
public ListenerRegistrar<Listener> getListenerRegistrar() {
return listenerManager;
}
/**
* Parses the given line synchronously, returning the tokens on the line.
*
* <p>This will NOT schedule parsing of subsequent lines.
*
* @return the parsed tokens, or {@code null} if there isn't a snapshot
* and it's not the first line
*/
@Nullable
public JsonArray<Token> parseLineSync(@Nonnull Line line) {
return worker.parseLine(line);
}
/**
* @return true if this parser mode supports smart indentation
*/
public boolean hasSmartIndent() {
return codeMirrorParser.hasSmartIndent();
}
/**
* Return the indentation for this line, based upon the line above it.
*/
public int getIndentation(Line line) {
return worker.getIndentation(line);
}
public void teardown() {
parserPosition.getLine().getDocument().getAnchorManager().removeAnchor(parserPosition);
textListenerRemover.remove();
scheduler.teardown();
}
void dispatchIterationStart(final int lineNumber) {
listenerManager.dispatch(new Dispatcher<Listener>() {
@Override
public void dispatch(Listener listener) {
listener.onIterationStart(lineNumber);
}
});
}
void dispatchIterationFinish() {
listenerManager.dispatch(new Dispatcher<Listener>() {
@Override
public void dispatch(Listener listener) {
listener.onIterationFinish();
}
});
}
void dispatch(final Line line, final int lineNumber, @Nonnull final JsonArray<Token> tokens) {
listenerManager.dispatch(new Dispatcher<Listener>() {
@Override
public void dispatch(Listener listener) {
listener.onDocumentLineParsed(line, lineNumber, tokens);
}
});
}
private boolean executeWorker(int workAmount) {
dispatchIterationStart(parserPosition.getLineNumber());
boolean result = worker.parse(parserPosition.getLine(), parserPosition.getLineNumber(),
workAmount, parserPosition);
dispatchIterationFinish();
return result;
}
public SyntaxType getSyntaxType() {
return codeMirrorParser.getSyntaxType();
}
/**
* Checks if line has been parsed since last changes (in this line or
* in lines above it).
*
* @param lineNumber number of line to check
* @return {@code true} if line is to be parsed.
*/
public boolean isLineDirty(int lineNumber) {
// Without this check last line never becomes "clean".
if (!scheduler.isBusy()) {
return false;
}
return parserPosition.getLineNumber() <= lineNumber;
}
/**
* Calculates parser mode at the beginning of line.
*
* @return {@code null} if previous line is not parsed yet
*/
@Nullable
public String getInitialMode(@Nonnull TaggableLine line) {
return worker.getInitialMode(line);
}
/**
* Synchronously parse beginning of the line and safely cast resulting state.
*
* <p>It is explicitly checked that current syntax type corresponds to
* specified state class.
*
* If the parser hasn't asynchronously reached previous line (may be it is
* appeared too recently) then {@code null} is returned.
*
* @see DocumentParserWorker#getParserState
*/
public <T extends State> ParseResult<T> getState(Class<T> stateClass, Position position,
@Nullable String appendedText) {
Preconditions.checkArgument(getSyntaxType().checkStateClass(stateClass));
Preconditions.checkNotNull(position);
return worker.getParserState(position, appendedText);
}
}