001    /*
002     * Copyright 2010-2016 JetBrains s.r.o.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    
017    package org.jetbrains.kotlin.cli.common.messages;
018    
019    import com.intellij.openapi.util.SystemInfo;
020    import com.intellij.util.LineSeparator;
021    import kotlin.text.StringsKt;
022    import org.fusesource.jansi.Ansi;
023    import org.fusesource.jansi.internal.CLibrary;
024    import org.jetbrains.annotations.NotNull;
025    import org.jetbrains.annotations.Nullable;
026    
027    import java.util.EnumSet;
028    import java.util.Set;
029    
030    import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.*;
031    
032    public abstract class PlainTextMessageRenderer implements MessageRenderer {
033    
034        private static final String KOTLIN_COLORS_ENABLED_PROPERTY = "kotlin.colors.enabled";
035    
036        // AnsiConsole doesn't check isatty() for stderr (see https://github.com/fusesource/jansi/pull/35).
037        // TODO: investigate why ANSI escape codes on Windows only work in REPL for some reason
038        public static final boolean COLOR_ENABLED =
039                !SystemInfo.isWindows &&
040                !"false".equals(System.getProperty(KOTLIN_COLORS_ENABLED_PROPERTY)) &&
041                CLibrary.isatty(CLibrary.STDERR_FILENO) != 0;
042    
043        private static final String LINE_SEPARATOR = LineSeparator.getSystemLineSeparator().getSeparatorString();
044    
045        private static final Set<CompilerMessageSeverity> IMPORTANT_MESSAGE_SEVERITIES = EnumSet.of(EXCEPTION, ERROR, WARNING);
046    
047        @Override
048        public String renderPreamble() {
049            return "";
050        }
051    
052        @Override
053        public String render(
054                @NotNull CompilerMessageSeverity severity, @NotNull String message, @NotNull CompilerMessageLocation location
055        ) {
056            StringBuilder result = new StringBuilder();
057    
058            int line = location.getLine();
059            int column = location.getColumn();
060            String lineContent = location.getLineContent();
061    
062            String path = getPath(location);
063            if (path != null) {
064                result.append(path);
065                result.append(":");
066                if (line > 0) {
067                    result.append(line).append(":");
068                    if (column > 0) {
069                        result.append(column).append(":");
070                    }
071                }
072                result.append(" ");
073            }
074    
075            if (COLOR_ENABLED) {
076                Ansi ansi = Ansi.ansi()
077                        .bold()
078                        .fg(severityColor(severity))
079                        .a(severity.name().toLowerCase())
080                        .a(": ")
081                        .reset();
082    
083                if (IMPORTANT_MESSAGE_SEVERITIES.contains(severity)) {
084                    ansi.bold();
085                }
086    
087                // Only make the first line of the message bold. Otherwise long overload ambiguity errors or exceptions are hard to read
088                String decapitalized = decapitalizeIfNeeded(message);
089                int firstNewline = decapitalized.indexOf(LINE_SEPARATOR);
090                if (firstNewline < 0) {
091                    result.append(ansi.a(decapitalized).reset());
092                }
093                else {
094                    result.append(ansi.a(decapitalized.substring(0, firstNewline)).reset().a(decapitalized.substring(firstNewline)));
095                }
096            }
097            else {
098                result.append(severity.name().toLowerCase());
099                result.append(": ");
100                result.append(decapitalizeIfNeeded(message));
101            }
102    
103            if (lineContent != null && 1 <= column && column <= lineContent.length() + 1) {
104                result.append(LINE_SEPARATOR);
105                result.append(lineContent);
106                result.append(LINE_SEPARATOR);
107                result.append(StringsKt.repeat(" ", column - 1));
108                result.append("^");
109            }
110    
111            return result.toString();
112        }
113    
114        @NotNull
115        private static String decapitalizeIfNeeded(@NotNull String message) {
116            // TODO: invent something more clever
117            // An ad-hoc heuristic to prevent decapitalization of some names
118            if (message.startsWith("Java") || message.startsWith("Kotlin")) {
119                return message;
120            }
121    
122            // For abbreviations and capitalized text
123            if (message.length() >= 2 && Character.isUpperCase(message.charAt(0)) && Character.isUpperCase(message.charAt(1))) {
124                return message;
125            }
126    
127            return StringsKt.decapitalize(message);
128        }
129    
130        @NotNull
131        private static Ansi.Color severityColor(@NotNull CompilerMessageSeverity severity) {
132            switch (severity) {
133                case EXCEPTION:
134                    return Ansi.Color.RED;
135                case ERROR:
136                    return Ansi.Color.RED;
137                case WARNING:
138                    return Ansi.Color.YELLOW;
139                case INFO:
140                    return Ansi.Color.BLUE;
141                case LOGGING:
142                    return Ansi.Color.BLUE;
143                case OUTPUT:
144                    return Ansi.Color.BLUE;
145                default:
146                    throw new UnsupportedOperationException("Unknown severity: " + severity);
147            }
148        }
149    
150        @Nullable
151        protected abstract String getPath(@NotNull CompilerMessageLocation location);
152    
153        @Override
154        public String renderConclusion() {
155            return "";
156        }
157    }