// 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.document.linedimensions;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.TextChange;
import com.google.collide.shared.util.RegExpUtils;
import com.google.collide.shared.util.SharedLogUtils;
import com.google.collide.shared.util.StringUtils;
import com.google.collide.shared.util.UnicodeUtils;
/**
* Various utility functions used by the {@link LineDimensionsCalculator}.
*
*/
public class LineDimensionsUtils {
/**
* Tag to identify if a line has an offset cache.
*/
final static String NEEDS_OFFSET_LINE_TAG =
LineDimensionsCalculator.class.getName() + "NEEDS_OFFSET";
/**
* The number of spaces a tab is treated as.
*/
// TODO: Delegate to EditorSettings once it is available.
private static int tabSpaceEquivalence = 2;
/**
* Enables or disables timeline marking.
*/
private static boolean enableLogging = false;
/**
* Checks if the line needs any offset other than indentation tabs and
* line-ending carriage-return.
*/
static boolean needsOffset(Line line) {
Boolean needsOffset = line.getTag(NEEDS_OFFSET_LINE_TAG);
// lets do a quick test if we haven't visited this line before
if (needsOffset == null) {
return forceUpdateNeedsOffset(line);
}
return needsOffset;
}
static boolean forceUpdateNeedsOffset(Line line) {
boolean needsOffset = hasSpecialCharactersWithExclusions(line);
line.putTag(NEEDS_OFFSET_LINE_TAG, needsOffset);
return needsOffset;
}
/**
* Uses the special character regex to determine if the line will need to be
* fully scanned.
*/
static boolean hasSpecialCharactersWithExclusions(Line line) {
return hasSpecialCharactersMaybeWithExclusions(line.getText(), true);
}
static boolean hasSpecialCharactersMaybeWithExclusions(String lineText, boolean stripTab) {
int length = lineText.length();
int index = stripTab ? getLastIndentationTabCount(lineText, length) : 0;
int endIndex = lineText.endsWith("\r\n") ? length - 2 : length;
return RegExpUtils.resetAndTest(
UnicodeUtils.regexpNonAsciiTabOrCarriageReturn, lineText.substring(index, endIndex));
}
/**
* Marks the lines internal cache of offset information dirty starting at the
* specified column. A column of 0 will clear the cache completely marking the
* line for re-measuring as needed.
*/
public static void isOffsetNeededAndCache(Line line, int column, TextChange.Type type) {
// TODO: Inspect the text instead of re-inspecting the entire line
markTimeline(LineDimensionsCalculator.class, "Begin maybe mark cache dirty");
Boolean hadNeedsOffset = line.getTag(NEEDS_OFFSET_LINE_TAG);
/*
* if you backspace the first character in a line, the line will be the
* previous line and type will be TextChange.Type.DELETE but that line could
* not have been visited yet. So we force a needOffset update in that case.
*/
if (hadNeedsOffset == null || (!hadNeedsOffset && type == TextChange.Type.INSERT)) {
forceUpdateNeedsOffset(line);
} else if (hadNeedsOffset) {
/*
* we don't know the zoom level here and am only going to mark the cache
* dirty so we can safely use getUnsafe.
*/
ColumnOffsetCache cache = ColumnOffsetCache.getUnsafe(line);
if (cache != null) {
// if the text was deleted, perhaps we removed the special characters?
if (type == TextChange.Type.DELETE && !forceUpdateNeedsOffset(line)) {
ColumnOffsetCache.removeFrom(line);
} else {
// if not we should mark our cache dirty
cache.markDirty(column);
}
}
}
markTimeline(LineDimensionsCalculator.class, "End maybe mark cache dirty");
}
public static void preTextIsOffsetNeededAndCache(
Line line, int column, TextChange.Type type, String text) {
// For delete we delegate through since the behavior is the same.
if (type == TextChange.Type.DELETE) {
isOffsetNeededAndCache(line, column, type);
} else {
Boolean hadNeedsOffset = line.getTag(NEEDS_OFFSET_LINE_TAG);
// If we are non-special case, scan the new text to determine if that's
// still the case.
if (hadNeedsOffset == null || !hadNeedsOffset) {
int newlineIndex = text.indexOf('\n');
String newText;
String oldText = line.getText();
if (newlineIndex == -1) {
// Inline insert.
newText = oldText.substring(0, column) + text + oldText.substring(column);
} else {
// Multi-line insert. We do not care about the following lines,
// as they will be processed later.
newText = oldText.substring(0, column) + text.substring(0, newlineIndex);
}
line.putTag(
NEEDS_OFFSET_LINE_TAG, hasSpecialCharactersMaybeWithExclusions(newText, true));
} else {
// We don't know the zoom level here and am only going to mark the cache
// dirty so we can safely use getUnsafe.
ColumnOffsetCache cache = ColumnOffsetCache.getUnsafe(line);
if (cache != null) {
cache.markDirty(column);
}
}
}
}
/**
* Returns the index of the last indentation tab before endColumn.
*/
public static int getLastIndentationTabCount(String lineText, int endColumn) {
int tabs = 0;
int length = lineText.length();
for (; tabs < length && lineText.charAt(tabs) == '\t' && tabs < endColumn; tabs++) {
// we do it all in the for loop :o
}
return tabs;
}
/**
* Sets the number of spaces a tab is rendered as.
*/
public static void setTabSpaceEquivalence(int spaces) {
tabSpaceEquivalence = spaces;
}
/**
* Retrieves the number of spaces a tab is rendered as.
*/
public static int getTabWidth() {
return tabSpaceEquivalence;
}
/**
* Returns a tab represented as a space.
*/
public static String getTabAsSpaces() {
return StringUtils.repeatString(" ", tabSpaceEquivalence);
}
/**
* Emits a markTimeline message if logging is enabled via
* {@link #enableLogging}.
*/
public static void markTimeline(Class<?> c, String message) {
if (enableLogging) {
SharedLogUtils.markTimeline(c, message);
}
}
}