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        public static final boolean COLOR_ENABLED;
034    
035        static {
036            boolean colorEnabled = false;
037            // TODO: investigate why ANSI escape codes on Windows only work in REPL for some reason
038            if (!SystemInfo.isWindows && !"false".equals(System.getProperty("kotlin.colors.enabled"))) {
039                try {
040                    // AnsiConsole doesn't check isatty() for stderr (see https://github.com/fusesource/jansi/pull/35).
041                    colorEnabled = CLibrary.isatty(CLibrary.STDERR_FILENO) != 0;
042                }
043                catch (UnsatisfiedLinkError e) {
044                    colorEnabled = false;
045                }
046            }
047            COLOR_ENABLED = colorEnabled;
048        }
049    
050        private static final String LINE_SEPARATOR = LineSeparator.getSystemLineSeparator().getSeparatorString();
051    
052        private static final Set<CompilerMessageSeverity> IMPORTANT_MESSAGE_SEVERITIES = EnumSet.of(EXCEPTION, ERROR, WARNING);
053    
054        @Override
055        public String renderPreamble() {
056            return "";
057        }
058    
059        @Override
060        public String render(
061                @NotNull CompilerMessageSeverity severity, @NotNull String message, @NotNull CompilerMessageLocation location
062        ) {
063            StringBuilder result = new StringBuilder();
064    
065            int line = location.getLine();
066            int column = location.getColumn();
067            String lineContent = location.getLineContent();
068    
069            String path = getPath(location);
070            if (path != null) {
071                result.append(path);
072                result.append(":");
073                if (line > 0) {
074                    result.append(line).append(":");
075                    if (column > 0) {
076                        result.append(column).append(":");
077                    }
078                }
079                result.append(" ");
080            }
081    
082            if (COLOR_ENABLED) {
083                Ansi ansi = Ansi.ansi()
084                        .bold()
085                        .fg(severityColor(severity))
086                        .a(severity.name().toLowerCase())
087                        .a(": ")
088                        .reset();
089    
090                if (IMPORTANT_MESSAGE_SEVERITIES.contains(severity)) {
091                    ansi.bold();
092                }
093    
094                // Only make the first line of the message bold. Otherwise long overload ambiguity errors or exceptions are hard to read
095                String decapitalized = decapitalizeIfNeeded(message);
096                int firstNewline = decapitalized.indexOf(LINE_SEPARATOR);
097                if (firstNewline < 0) {
098                    result.append(ansi.a(decapitalized).reset());
099                }
100                else {
101                    result.append(ansi.a(decapitalized.substring(0, firstNewline)).reset().a(decapitalized.substring(firstNewline)));
102                }
103            }
104            else {
105                result.append(severity.name().toLowerCase());
106                result.append(": ");
107                result.append(decapitalizeIfNeeded(message));
108            }
109    
110            if (lineContent != null && 1 <= column && column <= lineContent.length() + 1) {
111                result.append(LINE_SEPARATOR);
112                result.append(lineContent);
113                result.append(LINE_SEPARATOR);
114                result.append(StringsKt.repeat(" ", column - 1));
115                result.append("^");
116            }
117    
118            return result.toString();
119        }
120    
121        @NotNull
122        private static String decapitalizeIfNeeded(@NotNull String message) {
123            // TODO: invent something more clever
124            // An ad-hoc heuristic to prevent decapitalization of some names
125            if (message.startsWith("Java") || message.startsWith("Kotlin")) {
126                return message;
127            }
128    
129            // For abbreviations and capitalized text
130            if (message.length() >= 2 && Character.isUpperCase(message.charAt(0)) && Character.isUpperCase(message.charAt(1))) {
131                return message;
132            }
133    
134            return StringsKt.decapitalize(message);
135        }
136    
137        @NotNull
138        private static Ansi.Color severityColor(@NotNull CompilerMessageSeverity severity) {
139            switch (severity) {
140                case EXCEPTION:
141                    return Ansi.Color.RED;
142                case ERROR:
143                    return Ansi.Color.RED;
144                case WARNING:
145                    return Ansi.Color.YELLOW;
146                case INFO:
147                    return Ansi.Color.BLUE;
148                case LOGGING:
149                    return Ansi.Color.BLUE;
150                case OUTPUT:
151                    return Ansi.Color.BLUE;
152                default:
153                    throw new UnsupportedOperationException("Unknown severity: " + severity);
154            }
155        }
156    
157        @Nullable
158        protected abstract String getPath(@NotNull CompilerMessageLocation location);
159    
160        @Override
161        public String renderConclusion() {
162            return "";
163        }
164    }