// 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.code.errorrenderer; import com.google.collide.client.editor.Editor; import com.google.collide.client.editor.renderer.LineRenderer; import com.google.collide.client.util.logging.Log; import com.google.collide.dto.CodeError; import com.google.collide.dto.FilePosition; import com.google.collide.dto.client.DtoClientImpls; import com.google.collide.json.client.JsoArray; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.document.Line; import com.google.collide.shared.document.LineNumberAndColumn; import com.google.collide.shared.ot.PositionMigrator; import com.google.collide.shared.util.SortedList; /** * Renders code errors in the editor. */ public class ErrorRenderer implements LineRenderer { private static final SortedList.Comparator<CodeError> ERROR_COMPARATOR = new SortedList.Comparator<CodeError>() { @Override public int compare(CodeError error1, CodeError error2) { int startLineDiff = error1.getErrorStart().getLineNumber() - error2.getErrorStart().getLineNumber(); if (startLineDiff != 0) { return startLineDiff; } int startColumnDiff = error1.getErrorStart().getColumn() - error2.getErrorStart().getColumn(); if (startColumnDiff != 0) { return startColumnDiff; } int endLineDiff = error1.getErrorEnd().getLineNumber() - error2.getErrorEnd().getLineNumber(); if (endLineDiff != 0) { return endLineDiff; } else { return error1.getErrorEnd().getColumn() - error2.getErrorEnd().getColumn(); } } }; public JsonArray<CodeError> getCodeErrors() { return codeErrors; } private final Editor.Css css; private int currentLineNumber; private int currentLineLength; // Current render start position. private int linePosition; // Errors that are visible at current line. They may start on the previous line // (or even earlier) or end in one of the next lines. private SortedList<CodeError> lineErrors; // Index of next error to render in lineErrors array. private int nextErrorIndex; // List of errors for a file. private JsonArray<CodeError> codeErrors; private PositionMigrator positionMigrator; public ErrorRenderer(Editor.Resources res) { this.css = res.workspaceEditorCss(); codeErrors = JsoArray.create(); } @Override public void renderNextChunk(Target target) { CodeError nextError = getNextErrorToRender(); if (nextError == null) { // No errors to render. So render the rest of the line with null. renderNothingAndProceed(target, currentLineLength - linePosition); } else if (nextError.getErrorStart().getLineNumber() < currentLineNumber || nextError.getErrorStart().getColumn() == linePosition) { int errorLength; if (nextError.getErrorEnd().getLineNumber() > currentLineNumber) { errorLength = currentLineLength - linePosition; } else { // Error ends at current line. errorLength = nextError.getErrorEnd().getColumn() + 1 - linePosition; } renderErrorAndProceed(target, errorLength); } else { // Wait until we get to the next error. renderNothingAndProceed(target, nextError.getErrorStart().getColumn() - linePosition); } } @Override public boolean shouldLastChunkFillToRight() { return false; } private void renderErrorAndProceed(Target target, int characterCount) { Log.debug(getClass(), "Rendering " + characterCount + " characters with error style at position " + linePosition + ", next line position: " + (linePosition + characterCount)); target.render(characterCount, css.lineRendererError()); linePosition += characterCount; nextErrorIndex++; } private void renderNothingAndProceed(Target target, int characterCount) { target.render(characterCount, null); linePosition += characterCount; } private CodeError getNextErrorToRender() { while (nextErrorIndex < lineErrors.size()) { CodeError nextError = lineErrors.get(nextErrorIndex); if (nextError.getErrorEnd().getLineNumber() == currentLineNumber && nextError.getErrorEnd().getColumn() < linePosition) { // This may happen if errors overlap. nextErrorIndex++; continue; } else { return nextError; } } return null; } @Override public boolean resetToBeginningOfLine(Line line, int lineNumber) { // TODO: Convert to anchors so that error positions are updated when text edits happen. this.lineErrors = getErrorsAtLine(lineNumber); if (lineErrors.size() > 0) { Log.debug(getClass(), "Rendering line: " + lineNumber, ", errors size: " + lineErrors.size()); } else { return false; } this.currentLineNumber = lineNumber; this.currentLineLength = line.getText().length(); this.nextErrorIndex = 0; this.linePosition = 0; return true; } private SortedList<CodeError> getErrorsAtLine(int lineNumber) { int oldLineNumber = migrateLineNumber(lineNumber); SortedList<CodeError> result = new SortedList<CodeError>(ERROR_COMPARATOR); for (int i = 0; i < codeErrors.size(); i++) { CodeError error = codeErrors.get(i); if (error.getErrorStart().getLineNumber() <= oldLineNumber && error.getErrorEnd().getLineNumber() >= oldLineNumber) { result.add(migrateError(error)); } } return result; } private int migrateLineNumber(int lineNumber) { if (positionMigrator == null) { return lineNumber; } else { return positionMigrator.migrateFromNow(lineNumber, 0).lineNumber; } } private CodeError migrateError(CodeError oldError) { FilePosition newErrorStart = migrateFilePositionToNow(oldError.getErrorStart()); FilePosition newErrorEnd = migrateFilePositionToNow(oldError.getErrorEnd()); if (newErrorStart == oldError.getErrorStart() && newErrorEnd == oldError.getErrorEnd()) { return oldError; } DtoClientImpls.CodeErrorImpl newError = DtoClientImpls.CodeErrorImpl.make() .setErrorStart(newErrorStart) .setErrorEnd(newErrorEnd) .setMessage(oldError.getMessage()); Log.debug(getClass(), "Migrated error [" + codeErrorToString(oldError) + "] to [" + codeErrorToString(newError) + "]"); return newError; } private FilePosition migrateFilePositionToNow(FilePosition filePosition) { if (!positionMigrator.haveChanges()) { return filePosition; } LineNumberAndColumn newPosition = positionMigrator.migrateToNow(filePosition.getLineNumber(), filePosition.getColumn()); return DtoClientImpls.FilePositionImpl.make() .setLineNumber(newPosition.lineNumber) .setColumn(newPosition.column); } public void setCodeErrors(JsonArray<CodeError> codeErrors, PositionMigrator positionMigrator) { this.codeErrors = codeErrors; this.positionMigrator = positionMigrator; } private static String filePositionToString(FilePosition position) { return "(" + position.getLineNumber() + "," + position.getColumn() + ")"; } private static String codeErrorToString(CodeError codeError) { if (codeError == null) { return "null"; } else { return filePositionToString(codeError.getErrorStart()) + "-" + filePositionToString(codeError.getErrorEnd()); } } }