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 }