@parcel/codeframe 2.0.0-nightly.97 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
package/lib/codeframe.js CHANGED
@@ -5,58 +5,98 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = codeFrame;
7
7
 
8
- var _chalk = _interopRequireDefault(require("chalk"));
8
+ function _chalk() {
9
+ const data = _interopRequireDefault(require("chalk"));
9
10
 
10
- var _emphasize = _interopRequireDefault(require("emphasize"));
11
+ _chalk = function () {
12
+ return data;
13
+ };
14
+
15
+ return data;
16
+ }
17
+
18
+ function _emphasize() {
19
+ const data = _interopRequireDefault(require("emphasize"));
20
+
21
+ _emphasize = function () {
22
+ return data;
23
+ };
24
+
25
+ return data;
26
+ }
27
+
28
+ function _stringWidth() {
29
+ const data = _interopRequireDefault(require("string-width"));
30
+
31
+ _stringWidth = function () {
32
+ return data;
33
+ };
34
+
35
+ return data;
36
+ }
37
+
38
+ function _sliceAnsi() {
39
+ const data = _interopRequireDefault(require("slice-ansi"));
40
+
41
+ _sliceAnsi = function () {
42
+ return data;
43
+ };
44
+
45
+ return data;
46
+ }
11
47
 
12
48
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
49
 
14
50
  const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
15
51
  const TAB_REPLACE_REGEX = /\t/g;
16
52
  const TAB_REPLACEMENT = ' ';
53
+ const DEFAULT_TERMINAL_WIDTH = 80;
17
54
 
18
55
  const highlightSyntax = (txt, lang) => {
19
56
  if (lang) {
20
57
  try {
21
- return _emphasize.default.highlight(lang, txt).value;
58
+ return _emphasize().default.highlight(lang, txt).value;
22
59
  } catch (e) {// fallback for unknown languages...
23
60
  }
24
61
  }
25
62
 
26
- return _emphasize.default.highlightAuto(txt).value;
63
+ return _emphasize().default.highlightAuto(txt).value;
27
64
  };
28
65
 
29
- function codeFrame(code, highlights, // $FlowFixMe
30
- inputOpts = {}) {
66
+ function codeFrame(code, highlights, inputOpts = {}) {
67
+ var _inputOpts$maxLines;
68
+
31
69
  if (highlights.length < 1) return '';
32
70
  let opts = {
33
71
  useColor: !!inputOpts.useColor,
34
72
  syntaxHighlighting: !!inputOpts.syntaxHighlighting,
35
73
  language: inputOpts.language,
36
- maxLines: inputOpts.maxLines !== undefined ? inputOpts.maxLines : 12,
74
+ maxLines: (_inputOpts$maxLines = inputOpts.maxLines) !== null && _inputOpts$maxLines !== void 0 ? _inputOpts$maxLines : 12,
75
+ terminalWidth: inputOpts.terminalWidth || DEFAULT_TERMINAL_WIDTH,
37
76
  padding: inputOpts.padding || {
38
77
  before: 1,
39
78
  after: 2
40
79
  }
41
- };
80
+ }; // Highlights messages and prefixes when colors are enabled
42
81
 
43
82
  const highlighter = (s, bold) => {
44
83
  if (opts.useColor) {
45
- let redString = _chalk.default.red(s);
84
+ let redString = _chalk().default.red(s);
46
85
 
47
- return bold ? _chalk.default.bold(redString) : redString;
86
+ return bold ? _chalk().default.bold(redString) : redString;
48
87
  }
49
88
 
50
89
  return s;
51
- };
90
+ }; // Prefix lines with the line number
91
+
52
92
 
53
93
  const lineNumberPrefixer = params => {
54
94
  let {
55
95
  lineNumber,
56
- endLine,
96
+ lineNumberLength,
57
97
  isHighlighted
58
98
  } = params;
59
- return `${isHighlighted ? highlighter('>') : ' '} ${lineNumber ? lineNumber.padEnd(endLine.length, ' ') : ' '.repeat(endLine.length)} | `;
99
+ return `${isHighlighted ? highlighter('>') : ' '} ${lineNumber ? lineNumber.padStart(lineNumberLength, ' ') : ' '.repeat(lineNumberLength)} | `;
60
100
  }; // Make columns/lines start at 1
61
101
 
62
102
 
@@ -72,75 +112,127 @@ inputOpts = {}) {
72
112
  },
73
113
  message: h.message
74
114
  };
75
- });
115
+ }); // Find first and last highlight
116
+
76
117
  let firstHighlight = highlights.length > 1 ? highlights.sort((a, b) => a.start.line - b.start.line)[0] : highlights[0];
77
- let lastHighlight = highlights.length > 1 ? highlights.sort((a, b) => b.end.line - a.end.line)[0] : highlights[0];
118
+ let lastHighlight = highlights.length > 1 ? highlights.sort((a, b) => b.end.line - a.end.line)[0] : highlights[0]; // Calculate first and last line index of codeframe
119
+
78
120
  let startLine = firstHighlight.start.line - opts.padding.before;
79
121
  startLine = startLine < 0 ? 0 : startLine;
80
- let endLine = lastHighlight.end.line + opts.padding.after;
81
- endLine = endLine - startLine > opts.maxLines ? startLine + opts.maxLines - 1 : endLine;
82
- let endLineString = endLine.toString(10);
83
- let resultLines = [];
122
+ let endLineIndex = lastHighlight.end.line + opts.padding.after;
123
+ endLineIndex = endLineIndex - startLine > opts.maxLines ? startLine + opts.maxLines - 1 : endLineIndex;
124
+ let lineNumberLength = (endLineIndex + 1).toString(10).length; // Split input into lines and highlight syntax
125
+
84
126
  let lines = code.split(NEWLINE);
85
- let syntaxHighlightedLines = (opts.syntaxHighlighting ? highlightSyntax(code, opts.language) : code).replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).split(NEWLINE);
127
+ let syntaxHighlightedLines = (opts.syntaxHighlighting ? highlightSyntax(code, opts.language) : code).replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).split(NEWLINE); // Loop over all lines and create codeframe
128
+
129
+ let resultLines = [];
130
+
131
+ for (let currentLineIndex = startLine; currentLineIndex < syntaxHighlightedLines.length; currentLineIndex++) {
132
+ if (currentLineIndex > endLineIndex) break;
133
+ if (currentLineIndex > syntaxHighlightedLines.length - 1) break; // Find highlights that need to get rendered on the current line
134
+
135
+ let lineHighlights = highlights.filter(highlight => highlight.start.line <= currentLineIndex && highlight.end.line >= currentLineIndex).sort((a, b) => (a.start.line < currentLineIndex ? 0 : a.start.column) - (b.start.line < currentLineIndex ? 0 : b.start.column)); // Check if this line has a full line highlight
136
+
137
+ let isWholeLine = lineHighlights.length && !!lineHighlights.find(h => h.start.line < currentLineIndex && h.end.line > currentLineIndex);
138
+ let lineLengthLimit = opts.terminalWidth > lineNumberLength + 7 ? opts.terminalWidth - (lineNumberLength + 5) : 10; // Split the line into line parts that will fit the provided terminal width
139
+
140
+ let colOffset = 0;
141
+ let lineEndCol = lineLengthLimit;
142
+ let syntaxHighlightedLine = syntaxHighlightedLines[currentLineIndex];
143
+
144
+ if ((0, _stringWidth().default)(syntaxHighlightedLine) > lineLengthLimit) {
145
+ if (lineHighlights.length > 0) {
146
+ if (lineHighlights[0].start.line === currentLineIndex) {
147
+ colOffset = lineHighlights[0].start.column - 5;
148
+ } else if (lineHighlights[0].end.line === currentLineIndex) {
149
+ colOffset = lineHighlights[0].end.column - 5;
150
+ }
151
+ }
152
+
153
+ colOffset = colOffset > 0 ? colOffset : 0;
154
+ lineEndCol = colOffset + lineLengthLimit;
155
+ syntaxHighlightedLine = (0, _sliceAnsi().default)(syntaxHighlightedLine, colOffset, lineEndCol);
156
+ } // Write the syntax highlighted line part
157
+
86
158
 
87
- for (let i = startLine; i < lines.length; i++) {
88
- if (i > endLine) break;
89
- let originalLine = lines[i];
90
- let syntaxHighlightedLine = syntaxHighlightedLines[i];
91
- let foundHighlights = highlights.filter(highlight => highlight.start.line <= i && highlight.end.line >= i);
92
159
  resultLines.push(lineNumberPrefixer({
93
- lineNumber: (i + 1).toString(10),
94
- endLine: endLineString,
95
- isHighlighted: foundHighlights.length > 0
160
+ lineNumber: (currentLineIndex + 1).toString(10),
161
+ lineNumberLength,
162
+ isHighlighted: lineHighlights.length > 0
96
163
  }) + syntaxHighlightedLine);
164
+ let lineWidth = (0, _stringWidth().default)(syntaxHighlightedLine);
165
+ let highlightLine = '';
97
166
 
98
- if (foundHighlights.length > 0) {
99
- let highlightLine = lineNumberPrefixer({
100
- endLine: endLineString,
101
- isHighlighted: true
102
- });
103
- let isWholeLine = !!foundHighlights.find(h => h.start.line < i && h.end.line > i);
104
-
105
- if (isWholeLine) {
106
- // If there's a whole line highlight
107
- // don't even bother creating seperate highlight
108
- highlightLine += highlighter('^'.repeat(originalLine.replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).length));
109
- } else {
110
- let sortedColumns = foundHighlights.length > 1 ? foundHighlights.sort((a, b) => (a.start.line < i ? 0 : a.start.column) - (b.start.line < i ? 0 : b.start.column)) : foundHighlights;
111
- let lastCol = 0;
112
-
113
- for (let col of sortedColumns) {
114
- let startCol = col.start.line === i ? col.start.column : 0;
115
- let endCol = (col.end.line === i ? col.end.column : originalLine.length - (lastCol || 1)) + 1;
116
-
117
- if (startCol - lastCol > 0) {
118
- let whitespace = originalLine.substring(lastCol, startCol).replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).replace(/\S/g, ' ');
119
- highlightLine += whitespace;
120
- }
167
+ if (isWholeLine) {
168
+ highlightLine = highlighter('^'.repeat(lineWidth));
169
+ } else if (lineHighlights.length > 0) {
170
+ let lastCol = 0;
171
+ let highlight = null;
172
+ let highlightHasEnded = false;
121
173
 
122
- let highlightStartColumn = lastCol >= startCol ? lastCol : startCol;
174
+ for (let highlightIndex = 0; highlightIndex < lineHighlights.length; highlightIndex++) {
175
+ // Set highlight to current highlight
176
+ highlight = lineHighlights[highlightIndex];
177
+ highlightHasEnded = false; // Calculate the startColumn and get the real width by doing a substring of the original
178
+ // line and replacing tabs with our tab replacement to support tab handling
179
+
180
+ let startCol = 0;
181
+
182
+ if (highlight.start.line === currentLineIndex && highlight.start.column > colOffset) {
183
+ startCol = lines[currentLineIndex].substring(colOffset, highlight.start.column).replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).length;
184
+ } // Calculate the endColumn and get the real width by doing a substring of the original
185
+ // line and replacing tabs with our tab replacement to support tab handling
123
186
 
124
- if (endCol - highlightStartColumn > 0) {
125
- let renderedHighlightLength = originalLine.substring(highlightStartColumn, endCol).replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).length;
126
- highlightLine += highlighter('^'.repeat(renderedHighlightLength));
127
- lastCol = endCol;
128
- }
129
- }
130
187
 
131
- let endsWithFullLine = !!sortedColumns.find(h => h.end.line > i);
188
+ let endCol = lineWidth - 1;
132
189
 
133
- if (!endsWithFullLine) {
134
- let sortedByEnd = sortedColumns.sort((a, b) => a.end.column - b.end.column);
135
- let lastHighlightForColumn = sortedByEnd[sortedByEnd.length - 1];
190
+ if (highlight.end.line === currentLineIndex) {
191
+ endCol = lines[currentLineIndex].substring(colOffset, highlight.end.column).replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).length; // If the endCol is too big for this line part, trim it so we can handle it in the next one
136
192
 
137
- if (lastHighlightForColumn.message) {
138
- highlightLine += ' ' + highlighter(lastHighlightForColumn.message, true);
193
+ if (endCol > lineWidth) {
194
+ endCol = lineWidth - 1;
139
195
  }
196
+
197
+ highlightHasEnded = true;
198
+ } // If endcol is smaller than lastCol it overlaps with another highlight and is no longer visible, we can skip those
199
+
200
+
201
+ if (endCol >= lastCol) {
202
+ let characters = endCol - startCol + 1;
203
+
204
+ if (startCol > lastCol) {
205
+ // startCol is before lastCol, so add spaces as padding before the highlight indicators
206
+ highlightLine += ' '.repeat(startCol - lastCol);
207
+ } else if (lastCol > startCol) {
208
+ // If last column is larger than the start, there's overlap in highlights
209
+ // This line adjusts the characters count to ensure we don't add too many characters
210
+ characters += startCol - lastCol;
211
+ } // Append the highlight indicators
212
+
213
+
214
+ highlightLine += highlighter('^'.repeat(characters)); // Set the lastCol equal to character count between start of line part and highlight end-column
215
+
216
+ lastCol = endCol + 1;
217
+ } // There's no point in processing more highlights if we reached the end of the line
218
+
219
+
220
+ if (endCol >= lineEndCol - 1) {
221
+ break;
140
222
  }
223
+ } // Append the highlight message if the current highlights ends on this line part
224
+
225
+
226
+ if (highlight && highlight.message && highlightHasEnded) {
227
+ highlightLine += ' ' + highlighter(highlight.message, true);
141
228
  }
229
+ }
142
230
 
143
- resultLines.push(highlightLine);
231
+ if (highlightLine) {
232
+ resultLines.push(lineNumberPrefixer({
233
+ lineNumberLength,
234
+ isHighlighted: true
235
+ }) + highlightLine);
144
236
  }
145
237
  }
146
238
 
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@parcel/codeframe",
3
- "version": "2.0.0-nightly.97+a63f3fc9",
3
+ "version": "2.1.0",
4
4
  "description": "Blazing fast, zero configuration web application bundler",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
9
+ "funding": {
10
+ "type": "opencollective",
11
+ "url": "https://opencollective.com/parcel"
12
+ },
9
13
  "repository": {
10
14
  "type": "git",
11
15
  "url": "https://github.com/parcel-bundler/parcel.git"
@@ -13,11 +17,16 @@
13
17
  "main": "lib/codeframe.js",
14
18
  "source": "src/codeframe.js",
15
19
  "engines": {
16
- "node": ">= 10.0.0"
20
+ "node": ">= 12.0.0"
17
21
  },
18
22
  "dependencies": {
19
- "chalk": "^2.4.2",
20
- "emphasize": "^2.1.0"
23
+ "chalk": "^4.1.0",
24
+ "emphasize": "^4.2.0",
25
+ "slice-ansi": "^4.0.0",
26
+ "string-width": "^4.2.0"
27
+ },
28
+ "devDependencies": {
29
+ "strip-ansi": "^6.0.0"
21
30
  },
22
- "gitHead": "a63f3fc9726483219412920faeb255e035f90747"
31
+ "gitHead": "5a80fbe4af8797d0aab248a66f5254f42f00e83e"
23
32
  }
package/src/codeframe.js CHANGED
@@ -3,31 +3,29 @@ import type {DiagnosticCodeHighlight} from '@parcel/diagnostic';
3
3
 
4
4
  import chalk from 'chalk';
5
5
  import emphasize from 'emphasize';
6
+ import stringWidth from 'string-width';
7
+ import sliceAnsi from 'slice-ansi';
6
8
 
7
9
  type CodeFramePadding = {|
8
10
  before: number,
9
11
  after: number,
10
12
  |};
11
13
 
12
- type CodeFrameOptionsInput = {|
13
- useColor?: boolean,
14
- maxLines?: number,
15
- padding?: CodeFramePadding,
16
- syntaxHighlighting?: boolean,
17
- language?: string,
18
- |};
14
+ type CodeFrameOptionsInput = $Shape<CodeFrameOptions>;
19
15
 
20
16
  type CodeFrameOptions = {|
21
17
  useColor: boolean,
22
18
  syntaxHighlighting: boolean,
23
19
  maxLines: number,
24
20
  padding: CodeFramePadding,
21
+ terminalWidth: number,
25
22
  language?: string,
26
23
  |};
27
24
 
28
25
  const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
29
26
  const TAB_REPLACE_REGEX = /\t/g;
30
27
  const TAB_REPLACEMENT = ' ';
28
+ const DEFAULT_TERMINAL_WIDTH = 80;
31
29
 
32
30
  const highlightSyntax = (txt: string, lang?: string): string => {
33
31
  if (lang) {
@@ -44,7 +42,6 @@ const highlightSyntax = (txt: string, lang?: string): string => {
44
42
  export default function codeFrame(
45
43
  code: string,
46
44
  highlights: Array<DiagnosticCodeHighlight>,
47
- // $FlowFixMe
48
45
  inputOpts: CodeFrameOptionsInput = {},
49
46
  ): string {
50
47
  if (highlights.length < 1) return '';
@@ -53,13 +50,15 @@ export default function codeFrame(
53
50
  useColor: !!inputOpts.useColor,
54
51
  syntaxHighlighting: !!inputOpts.syntaxHighlighting,
55
52
  language: inputOpts.language,
56
- maxLines: inputOpts.maxLines !== undefined ? inputOpts.maxLines : 12,
53
+ maxLines: inputOpts.maxLines ?? 12,
54
+ terminalWidth: inputOpts.terminalWidth || DEFAULT_TERMINAL_WIDTH,
57
55
  padding: inputOpts.padding || {
58
56
  before: 1,
59
57
  after: 2,
60
58
  },
61
59
  };
62
60
 
61
+ // Highlights messages and prefixes when colors are enabled
63
62
  const highlighter = (s: string, bold?: boolean) => {
64
63
  if (opts.useColor) {
65
64
  let redString = chalk.red(s);
@@ -69,17 +68,18 @@ export default function codeFrame(
69
68
  return s;
70
69
  };
71
70
 
71
+ // Prefix lines with the line number
72
72
  const lineNumberPrefixer = (params: {|
73
73
  lineNumber?: string,
74
- endLine: string,
74
+ lineNumberLength: number,
75
75
  isHighlighted: boolean,
76
76
  |}) => {
77
- let {lineNumber, endLine, isHighlighted} = params;
77
+ let {lineNumber, lineNumberLength, isHighlighted} = params;
78
78
 
79
79
  return `${isHighlighted ? highlighter('>') : ' '} ${
80
80
  lineNumber
81
- ? lineNumber.padEnd(endLine.length, ' ')
82
- : ' '.repeat(endLine.length)
81
+ ? lineNumber.padStart(lineNumberLength, ' ')
82
+ : ' '.repeat(lineNumberLength)
83
83
  } | `;
84
84
  };
85
85
 
@@ -98,6 +98,7 @@ export default function codeFrame(
98
98
  };
99
99
  });
100
100
 
101
+ // Find first and last highlight
101
102
  let firstHighlight =
102
103
  highlights.length > 1
103
104
  ? highlights.sort((a, b) => a.start.line - b.start.line)[0]
@@ -107,111 +108,176 @@ export default function codeFrame(
107
108
  ? highlights.sort((a, b) => b.end.line - a.end.line)[0]
108
109
  : highlights[0];
109
110
 
111
+ // Calculate first and last line index of codeframe
110
112
  let startLine = firstHighlight.start.line - opts.padding.before;
111
113
  startLine = startLine < 0 ? 0 : startLine;
112
- let endLine = lastHighlight.end.line + opts.padding.after;
113
- endLine =
114
- endLine - startLine > opts.maxLines
114
+ let endLineIndex = lastHighlight.end.line + opts.padding.after;
115
+ endLineIndex =
116
+ endLineIndex - startLine > opts.maxLines
115
117
  ? startLine + opts.maxLines - 1
116
- : endLine;
117
- let endLineString = endLine.toString(10);
118
+ : endLineIndex;
118
119
 
119
- let resultLines = [];
120
+ let lineNumberLength = (endLineIndex + 1).toString(10).length;
121
+
122
+ // Split input into lines and highlight syntax
120
123
  let lines = code.split(NEWLINE);
121
- let syntaxHighlightedLines = (opts.syntaxHighlighting
122
- ? highlightSyntax(code, opts.language)
123
- : code
124
+ let syntaxHighlightedLines = (
125
+ opts.syntaxHighlighting ? highlightSyntax(code, opts.language) : code
124
126
  )
125
127
  .replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT)
126
128
  .split(NEWLINE);
127
- for (let i = startLine; i < lines.length; i++) {
128
- if (i > endLine) break;
129
129
 
130
- let originalLine: string = lines[i];
131
- let syntaxHighlightedLine = syntaxHighlightedLines[i];
130
+ // Loop over all lines and create codeframe
131
+ let resultLines = [];
132
+ for (
133
+ let currentLineIndex = startLine;
134
+ currentLineIndex < syntaxHighlightedLines.length;
135
+ currentLineIndex++
136
+ ) {
137
+ if (currentLineIndex > endLineIndex) break;
138
+ if (currentLineIndex > syntaxHighlightedLines.length - 1) break;
132
139
 
133
- let foundHighlights = highlights.filter(
134
- highlight => highlight.start.line <= i && highlight.end.line >= i,
135
- );
140
+ // Find highlights that need to get rendered on the current line
141
+ let lineHighlights = highlights
142
+ .filter(
143
+ highlight =>
144
+ highlight.start.line <= currentLineIndex &&
145
+ highlight.end.line >= currentLineIndex,
146
+ )
147
+ .sort(
148
+ (a, b) =>
149
+ (a.start.line < currentLineIndex ? 0 : a.start.column) -
150
+ (b.start.line < currentLineIndex ? 0 : b.start.column),
151
+ );
152
+
153
+ // Check if this line has a full line highlight
154
+ let isWholeLine =
155
+ lineHighlights.length &&
156
+ !!lineHighlights.find(
157
+ h => h.start.line < currentLineIndex && h.end.line > currentLineIndex,
158
+ );
159
+
160
+ let lineLengthLimit =
161
+ opts.terminalWidth > lineNumberLength + 7
162
+ ? opts.terminalWidth - (lineNumberLength + 5)
163
+ : 10;
164
+
165
+ // Split the line into line parts that will fit the provided terminal width
166
+ let colOffset = 0;
167
+ let lineEndCol = lineLengthLimit;
168
+ let syntaxHighlightedLine = syntaxHighlightedLines[currentLineIndex];
169
+ if (stringWidth(syntaxHighlightedLine) > lineLengthLimit) {
170
+ if (lineHighlights.length > 0) {
171
+ if (lineHighlights[0].start.line === currentLineIndex) {
172
+ colOffset = lineHighlights[0].start.column - 5;
173
+ } else if (lineHighlights[0].end.line === currentLineIndex) {
174
+ colOffset = lineHighlights[0].end.column - 5;
175
+ }
176
+ }
177
+
178
+ colOffset = colOffset > 0 ? colOffset : 0;
179
+ lineEndCol = colOffset + lineLengthLimit;
136
180
 
181
+ syntaxHighlightedLine = sliceAnsi(
182
+ syntaxHighlightedLine,
183
+ colOffset,
184
+ lineEndCol,
185
+ );
186
+ }
187
+
188
+ // Write the syntax highlighted line part
137
189
  resultLines.push(
138
190
  lineNumberPrefixer({
139
- lineNumber: (i + 1).toString(10),
140
- endLine: endLineString,
141
- isHighlighted: foundHighlights.length > 0,
191
+ lineNumber: (currentLineIndex + 1).toString(10),
192
+ lineNumberLength,
193
+ isHighlighted: lineHighlights.length > 0,
142
194
  }) + syntaxHighlightedLine,
143
195
  );
144
196
 
145
- if (foundHighlights.length > 0) {
146
- let highlightLine = lineNumberPrefixer({
147
- endLine: endLineString,
148
- isHighlighted: true,
149
- });
197
+ let lineWidth = stringWidth(syntaxHighlightedLine);
198
+ let highlightLine = '';
199
+ if (isWholeLine) {
200
+ highlightLine = highlighter('^'.repeat(lineWidth));
201
+ } else if (lineHighlights.length > 0) {
202
+ let lastCol = 0;
203
+ let highlight = null;
204
+ let highlightHasEnded = false;
150
205
 
151
- let isWholeLine = !!foundHighlights.find(
152
- h => h.start.line < i && h.end.line > i,
153
- );
206
+ for (
207
+ let highlightIndex = 0;
208
+ highlightIndex < lineHighlights.length;
209
+ highlightIndex++
210
+ ) {
211
+ // Set highlight to current highlight
212
+ highlight = lineHighlights[highlightIndex];
213
+ highlightHasEnded = false;
154
214
 
155
- if (isWholeLine) {
156
- // If there's a whole line highlight
157
- // don't even bother creating seperate highlight
158
- highlightLine += highlighter(
159
- '^'.repeat(
160
- originalLine.replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).length,
161
- ),
162
- );
163
- } else {
164
- let sortedColumns =
165
- foundHighlights.length > 1
166
- ? foundHighlights.sort(
167
- (a, b) =>
168
- (a.start.line < i ? 0 : a.start.column) -
169
- (b.start.line < i ? 0 : b.start.column),
170
- )
171
- : foundHighlights;
172
-
173
- let lastCol = 0;
174
- for (let col of sortedColumns) {
175
- let startCol = col.start.line === i ? col.start.column : 0;
176
- let endCol =
177
- (col.end.line === i
178
- ? col.end.column
179
- : originalLine.length - (lastCol || 1)) + 1;
180
-
181
- if (startCol - lastCol > 0) {
182
- let whitespace = originalLine
183
- .substring(lastCol, startCol)
184
- .replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT)
185
- .replace(/\S/g, ' ');
186
-
187
- highlightLine += whitespace;
188
- }
215
+ // Calculate the startColumn and get the real width by doing a substring of the original
216
+ // line and replacing tabs with our tab replacement to support tab handling
217
+ let startCol = 0;
218
+ if (
219
+ highlight.start.line === currentLineIndex &&
220
+ highlight.start.column > colOffset
221
+ ) {
222
+ startCol = lines[currentLineIndex]
223
+ .substring(colOffset, highlight.start.column)
224
+ .replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).length;
225
+ }
189
226
 
190
- let highlightStartColumn = lastCol >= startCol ? lastCol : startCol;
191
- if (endCol - highlightStartColumn > 0) {
192
- let renderedHighlightLength = originalLine
193
- .substring(highlightStartColumn, endCol)
194
- .replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).length;
195
- highlightLine += highlighter('^'.repeat(renderedHighlightLength));
196
- lastCol = endCol;
227
+ // Calculate the endColumn and get the real width by doing a substring of the original
228
+ // line and replacing tabs with our tab replacement to support tab handling
229
+ let endCol = lineWidth - 1;
230
+ if (highlight.end.line === currentLineIndex) {
231
+ endCol = lines[currentLineIndex]
232
+ .substring(colOffset, highlight.end.column)
233
+ .replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).length;
234
+
235
+ // If the endCol is too big for this line part, trim it so we can handle it in the next one
236
+ if (endCol > lineWidth) {
237
+ endCol = lineWidth - 1;
197
238
  }
198
- }
199
239
 
200
- let endsWithFullLine = !!sortedColumns.find(h => h.end.line > i);
201
- if (!endsWithFullLine) {
202
- let sortedByEnd = sortedColumns.sort(
203
- (a, b) => a.end.column - b.end.column,
204
- );
240
+ highlightHasEnded = true;
241
+ }
205
242
 
206
- let lastHighlightForColumn = sortedByEnd[sortedByEnd.length - 1];
207
- if (lastHighlightForColumn.message) {
208
- highlightLine +=
209
- ' ' + highlighter(lastHighlightForColumn.message, true);
243
+ // If endcol is smaller than lastCol it overlaps with another highlight and is no longer visible, we can skip those
244
+ if (endCol >= lastCol) {
245
+ let characters = endCol - startCol + 1;
246
+ if (startCol > lastCol) {
247
+ // startCol is before lastCol, so add spaces as padding before the highlight indicators
248
+ highlightLine += ' '.repeat(startCol - lastCol);
249
+ } else if (lastCol > startCol) {
250
+ // If last column is larger than the start, there's overlap in highlights
251
+ // This line adjusts the characters count to ensure we don't add too many characters
252
+ characters += startCol - lastCol;
210
253
  }
254
+
255
+ // Append the highlight indicators
256
+ highlightLine += highlighter('^'.repeat(characters));
257
+
258
+ // Set the lastCol equal to character count between start of line part and highlight end-column
259
+ lastCol = endCol + 1;
260
+ }
261
+
262
+ // There's no point in processing more highlights if we reached the end of the line
263
+ if (endCol >= lineEndCol - 1) {
264
+ break;
211
265
  }
212
266
  }
213
267
 
214
- resultLines.push(highlightLine);
268
+ // Append the highlight message if the current highlights ends on this line part
269
+ if (highlight && highlight.message && highlightHasEnded) {
270
+ highlightLine += ' ' + highlighter(highlight.message, true);
271
+ }
272
+ }
273
+
274
+ if (highlightLine) {
275
+ resultLines.push(
276
+ lineNumberPrefixer({
277
+ lineNumberLength,
278
+ isHighlighted: true,
279
+ }) + highlightLine,
280
+ );
215
281
  }
216
282
  }
217
283
 
@@ -1,4 +1,6 @@
1
1
  import assert from 'assert';
2
+ import {readFileSync} from 'fs';
3
+ import {join as joinPath} from 'path';
2
4
 
3
5
  import codeframe from '../src/codeframe';
4
6
 
@@ -393,7 +395,7 @@ describe('codeframe', () => {
393
395
  assert.equal(lines[7], ' 9 | test');
394
396
  });
395
397
 
396
- it('should properly pad numbers', () => {
398
+ it('should properly pad numbers for large files', () => {
397
399
  let codeframeString = codeframe('test\n'.repeat(1000), [
398
400
  {
399
401
  start: {
@@ -415,17 +417,22 @@ describe('codeframe', () => {
415
417
  column: 2,
416
418
  line: 100,
417
419
  },
418
- message: 'test',
420
+ message: 'test 2',
419
421
  },
420
422
  ]);
421
423
 
422
424
  let lines = codeframeString.split(LINE_END);
423
425
  assert.equal(lines.length, 7);
424
- assert.equal(lines[0], ' 98 | test');
426
+ assert.equal(lines[0], ' 98 | test');
427
+ assert.equal(lines[1], '> 99 | test');
428
+ assert.equal(lines[2], '> | ^ test');
429
+ assert.equal(lines[3], '> 100 | test');
430
+ assert.equal(lines[4], '> | ^ test 2');
431
+ assert.equal(lines[5], ' 101 | test');
425
432
  assert.equal(lines[6], ' 102 | test');
426
433
  });
427
434
 
428
- it('should properly pad numbers', () => {
435
+ it('should properly pad numbers for short files', () => {
429
436
  let codeframeString = codeframe('test\n'.repeat(1000), [
430
437
  {
431
438
  start: {
@@ -453,13 +460,17 @@ describe('codeframe', () => {
453
460
 
454
461
  let lines = codeframeString.split(LINE_END);
455
462
  assert.equal(lines.length, 11);
456
- assert.equal(lines[0], ' 6 | test');
463
+ assert.equal(lines[0], ' 6 | test');
464
+ assert.equal(lines[4], ' 9 | test');
465
+ assert.equal(lines[5], ' 10 | test');
466
+ assert.equal(lines[6], ' 11 | test');
457
467
  assert.equal(lines[10], ' 14 | test');
458
468
  });
459
469
 
460
470
  it('should properly use maxLines', () => {
471
+ let line = 'test '.repeat(100);
461
472
  let codeframeString = codeframe(
462
- 'test\n'.repeat(100),
473
+ `${line}\n`.repeat(100),
463
474
  [
464
475
  {
465
476
  start: {
@@ -487,14 +498,16 @@ describe('codeframe', () => {
487
498
  {
488
499
  useColor: false,
489
500
  maxLines: 10,
501
+ terminalWidth: 5,
490
502
  },
491
503
  );
492
504
 
493
505
  let lines = codeframeString.split(LINE_END);
494
506
  assert.equal(lines.length, 13);
495
- assert.equal(lines[0], ' 4 | test');
496
- assert.equal(lines[11], '> 13 | test');
497
- assert.equal(lines[12], '> | ^^^^');
507
+ assert.equal(lines[0], ' 4 | test test ');
508
+ assert.equal(lines[7], ' 10 | test test ');
509
+ assert.equal(lines[11], '> 13 | test test ');
510
+ assert.equal(lines[12], '> | ^^^^^^^^^^');
498
511
  });
499
512
 
500
513
  it('should be able to handle tabs', () => {
@@ -507,7 +520,7 @@ describe('codeframe', () => {
507
520
  line: 1,
508
521
  },
509
522
  end: {
510
- column: 7,
523
+ column: 8,
511
524
  line: 1,
512
525
  },
513
526
  message: 'test',
@@ -518,7 +531,7 @@ describe('codeframe', () => {
518
531
 
519
532
  let lines = codeframeString.split(LINE_END);
520
533
  assert.equal(lines[0], '> 1 | hel lo wor ld');
521
- assert.equal(lines[1], '> | ^^^ test');
534
+ assert.equal(lines[1], '> | ^^^^ test');
522
535
  assert.equal(lines[2], ' 2 | Enjoy thi s nice cod eframe');
523
536
  });
524
537
 
@@ -585,4 +598,202 @@ describe('codeframe', () => {
585
598
  assert.equal(lines[4], '> 3 | test');
586
599
  assert.equal(lines[5], '> | ^^ test');
587
600
  });
601
+
602
+ it('Should truncate long lines and print message', () => {
603
+ let originalLine = 'hello world '.repeat(1000);
604
+ let codeframeString = codeframe(
605
+ originalLine,
606
+ [
607
+ {
608
+ start: {
609
+ column: 1000,
610
+ line: 1,
611
+ },
612
+ end: {
613
+ column: 1200,
614
+ line: 1,
615
+ },
616
+ message: 'This is a message',
617
+ },
618
+ ],
619
+ {useColor: false, terminalWidth: 25},
620
+ );
621
+
622
+ let lines = codeframeString.split(LINE_END);
623
+ assert.equal(lines.length, 2);
624
+ assert.equal(lines[0], '> 1 | d hello world hello');
625
+ assert.equal(lines[1], '> | ^^^^^^^^^^^^^^ This is a message');
626
+ });
627
+
628
+ it('Truncation across multiple lines', () => {
629
+ let originalLine =
630
+ 'hello world '.repeat(100) + '\n' + 'new line '.repeat(100);
631
+ let codeframeString = codeframe(
632
+ originalLine,
633
+ [
634
+ {
635
+ start: {
636
+ column: 15,
637
+ line: 1,
638
+ },
639
+ end: {
640
+ column: 400,
641
+ line: 1,
642
+ },
643
+ message: 'This is the first line',
644
+ },
645
+ {
646
+ start: {
647
+ column: 2,
648
+ line: 2,
649
+ },
650
+ end: {
651
+ column: 100,
652
+ line: 2,
653
+ },
654
+ message: 'This is the second line',
655
+ },
656
+ ],
657
+ {useColor: false, terminalWidth: 25},
658
+ );
659
+
660
+ let lines = codeframeString.split(LINE_END);
661
+ assert.equal(lines.length, 4);
662
+ assert.equal(lines[0], '> 1 | ld hello world hell');
663
+ assert.equal(lines[1], '> | ^^^^^^^^^^^^^^ This is the first line');
664
+ assert.equal(lines[2], '> 2 | new line new line n');
665
+ assert.equal(lines[3], '> | ^^^^^^^^^^^^^^^^^^ This is the second line');
666
+ });
667
+
668
+ it('Truncation across various types and positions of highlights', () => {
669
+ let originalLine =
670
+ 'hello world '.repeat(100) + '\n' + 'new line '.repeat(100);
671
+ let codeframeString = codeframe(
672
+ originalLine,
673
+ [
674
+ {
675
+ start: {
676
+ column: 2,
677
+ line: 1,
678
+ },
679
+ end: {
680
+ column: 5,
681
+ line: 1,
682
+ },
683
+ },
684
+ {
685
+ start: {
686
+ column: 6,
687
+ line: 1,
688
+ },
689
+ end: {
690
+ column: 10,
691
+ line: 1,
692
+ },
693
+ message: 'I have a message',
694
+ },
695
+ {
696
+ start: {
697
+ column: 15,
698
+ line: 1,
699
+ },
700
+ end: {
701
+ column: 25,
702
+ line: 1,
703
+ },
704
+ message: 'I also have a message',
705
+ },
706
+ {
707
+ start: {
708
+ column: 2,
709
+ line: 2,
710
+ },
711
+ end: {
712
+ column: 5,
713
+ line: 2,
714
+ },
715
+ message: 'This is the second line',
716
+ },
717
+ ],
718
+ {useColor: false, terminalWidth: 25},
719
+ );
720
+
721
+ let lines = codeframeString.split(LINE_END);
722
+ assert.equal(lines.length, 4);
723
+ assert.equal(lines[0], '> 1 | hello world hello w');
724
+ assert.equal(lines[1], '> | ^^^^^^^^^ ^^^^^ I also have a message');
725
+ assert.equal(lines[2], '> 2 | new line new line n');
726
+ assert.equal(lines[3], '> | ^^^^ This is the second line');
727
+ });
728
+
729
+ it('Multi-line highlight w/ truncation', () => {
730
+ let originalLine =
731
+ 'hello world '.repeat(100) + '\n' + 'new line '.repeat(100);
732
+ let codeframeString = codeframe(
733
+ originalLine,
734
+ [
735
+ {
736
+ start: {
737
+ column: 2,
738
+ line: 1,
739
+ },
740
+ end: {
741
+ column: 151,
742
+ line: 2,
743
+ },
744
+ message: 'I have a message',
745
+ },
746
+ ],
747
+ {useColor: false, terminalWidth: 25},
748
+ );
749
+
750
+ let lines = codeframeString.split(LINE_END);
751
+ assert.equal(lines.length, 4);
752
+ assert.equal(lines[0], '> 1 | hello world hello w');
753
+ assert.equal(lines[1], '> | ^^^^^^^^^^^^^^^^^^');
754
+ assert.equal(lines[2], '> 2 | ew line new line ne');
755
+ assert.equal(lines[3], '> | ^^^^^^ I have a message');
756
+ });
757
+
758
+ it('Should pad properly, T-650', () => {
759
+ let fileContent = readFileSync(
760
+ joinPath(__dirname, './fixtures/a.js'),
761
+ 'utf8',
762
+ );
763
+ let codeframeString = codeframe(
764
+ fileContent,
765
+ [
766
+ {
767
+ start: {
768
+ line: 8,
769
+ column: 10,
770
+ },
771
+ end: {
772
+ line: 8,
773
+ column: 48,
774
+ },
775
+ },
776
+ ],
777
+ {
778
+ useColor: false,
779
+ syntaxHighlighting: false,
780
+ language: 'js',
781
+ terminalWidth: 100,
782
+ },
783
+ );
784
+
785
+ let lines = codeframeString.split(LINE_END);
786
+ assert.equal(lines.length, 5);
787
+ assert.equal(lines[0], ` 7 | import Tooltip from '../tooltip';`);
788
+ assert.equal(
789
+ lines[1],
790
+ `> 8 | import VisuallyHidden from '../visually-hidden';`,
791
+ );
792
+ assert.equal(
793
+ lines[2],
794
+ '> | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^',
795
+ );
796
+ assert.equal(lines[3], ' 9 | ');
797
+ assert.equal(lines[4], ' 10 | /**');
798
+ });
588
799
  });
@@ -0,0 +1,13 @@
1
+ import test from 'test';
2
+ import component from './component';
3
+
4
+ /**
5
+ * This is a comment
6
+ */
7
+ import Tooltip from '../tooltip';
8
+ import VisuallyHidden from '../visually-hidden';
9
+
10
+ /**
11
+ * This is another comment
12
+ */
13
+ import {Label} from './label';