// 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.autoindenter; import com.google.collide.client.documentparser.DocumentParser; import com.google.collide.client.editor.Editor; import com.google.collide.client.editor.EditorDocumentMutator; import com.google.collide.codemirror2.SyntaxType; import com.google.collide.shared.document.Line; import com.google.collide.shared.document.TextChange; import com.google.collide.shared.document.util.LineUtils; import com.google.collide.shared.util.StringUtils; import com.google.collide.shared.util.TextUtils; import com.google.collide.shared.util.ListenerRegistrar.Remover; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.regexp.shared.RegExp; /** * A class responsible for automatically adding indentation when appropriate. */ public class Autoindenter { private static final RegExp WHITESPACES = RegExp.compile("^\\s*$"); private interface IndentationStrategy { Runnable handleTextChange(TextChange textChange, EditorDocumentMutator editorDocumentMutator); } private static class PreviousLineMatchingIndentationStrategy implements IndentationStrategy { @Override public Runnable handleTextChange( TextChange textChange, final EditorDocumentMutator editorDocumentMutator) { String text = textChange.getText(); if (!"\n".equals(text)) { return null; } final int toInsert = TextUtils.countWhitespacesAtTheBeginningOfLine( textChange.getLine().getText()); if (toInsert == 0) { return null; } final Line line = LineUtils.getLine(textChange.getLine(), 1); final int lineNumber = textChange.getLineNumber() + 1; return new Runnable() { @Override public void run() { String addend = StringUtils.getSpaces(toInsert); editorDocumentMutator.insertText(line, lineNumber, 0, addend); } }; } } private static class CodeMirrorIndentationStrategy implements IndentationStrategy { private final DocumentParser documentParser; CodeMirrorIndentationStrategy(DocumentParser parser) { documentParser = parser; } @Override public Runnable handleTextChange( TextChange textChange, final EditorDocumentMutator editorDocumentMutator) { String text = textChange.getText(); if (!"\n".equals(text)) { // TODO: We should incrementally apply autoindention to // multiline pastes. // TODO: Take electric characters into account: // documentParser.getElectricCharacters. return null; } // TODO: Ask parser to reparse changed line. final Line line = LineUtils.getLine(textChange.getLine(), 1); // Special case: pressing ENTER in the middle of whitespaces line should // not fix indentation (use case: press ENTER on empty line). Line prevLine = textChange.getLine(); if (WHITESPACES.test(prevLine.getText()) && WHITESPACES.test(line.getText())) { return null; } final int lineNumber = textChange.getLineNumber() + 1; final int indentation = documentParser.getIndentation(line); if (indentation < 0) { return null; } final int oldIndentation = TextUtils.countWhitespacesAtTheBeginningOfLine(line.getText()); if (indentation == oldIndentation) { return null; } return new Runnable() { @Override public void run() { if (indentation < oldIndentation) { editorDocumentMutator.deleteText(line, lineNumber, 0, oldIndentation - indentation); } else { String addend = StringUtils.getSpaces(indentation - oldIndentation); editorDocumentMutator.insertText(line, lineNumber, 0, addend); } } }; } } /** * Creates an instance of {@link Autoindenter} that is configured to take on * the appropriate indentation strategy depending on the document parser. */ public static Autoindenter create(DocumentParser documentParser, Editor editor) { if (documentParser.getSyntaxType() != SyntaxType.NONE && documentParser.hasSmartIndent()) { return new Autoindenter(new CodeMirrorIndentationStrategy(documentParser), editor); } return new Autoindenter(new PreviousLineMatchingIndentationStrategy(), editor); } private final Editor editor; private final IndentationStrategy indentationStrategy; private boolean isMutatingDocument; private final Editor.TextListener textListener = new Editor.TextListener() { @Override public void onTextChange(TextChange textChange) { handleTextChange(textChange); } }; private final Remover textListenerRemover; private Autoindenter(IndentationStrategy indentationStrategy, Editor editor) { this.indentationStrategy = indentationStrategy; this.editor = editor; textListenerRemover = editor.getTextListenerRegistrar().add(textListener); } public void teardown() { textListenerRemover.remove(); } private void handleTextChange(TextChange textChange) { if (isMutatingDocument || editor.isMutatingDocumentFromUndoOrRedo() || textChange.getType() != TextChange.Type.INSERT) { return; } final Runnable mutator = indentationStrategy.handleTextChange( textChange, editor.getEditorDocumentMutator()); if (mutator == null) { return; } // We shouldn't be touching the document in this callback, so defer. Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { isMutatingDocument = true; try { mutator.run(); } finally { isMutatingDocument = false; } } }); } }