Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
60.00% |
15 / 25 |
CRAP | |
38.96% |
90 / 231 |
Row | |
0.00% |
0 / 1 |
|
60.00% |
15 / 25 |
2420.87 | |
38.96% |
90 / 231 |
new | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
default | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 4 |
|||
__construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
__get | |
100.00% |
1 / 1 |
1 | |
100.00% |
5 / 5 |
|||
__toString | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
__debugInfo | |
100.00% |
1 / 1 |
1 | |
100.00% |
6 / 6 |
|||
isValid | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
insert | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
|||
append | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
delete | |
100.00% |
1 / 1 |
3 | |
100.00% |
4 / 4 |
|||
setChars | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
update | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
highlight | |
0.00% |
0 / 1 |
52.21 | |
56.82% |
25 / 44 |
|||
highlightNumber | |
0.00% |
0 / 1 |
52.45 | |
18.75% |
3 / 16 |
|||
highlightWord | |
0.00% |
0 / 1 |
16.14 | |
42.86% |
6 / 14 |
|||
highlightChar | |
0.00% |
0 / 1 |
2.50 | |
50.00% |
3 / 6 |
|||
highlightPrimaryKeywords | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
highlightSecondaryKeywords | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
highlightOperators | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
highlightCommonOperators | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
highlightCommonDelimeters | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
highlightCharacter | |
0.00% |
0 / 1 |
17.58 | |
40.00% |
6 / 15 |
|||
highlightComment | |
0.00% |
0 / 1 |
11.53 | |
22.22% |
2 / 9 |
|||
highlightString | |
0.00% |
0 / 1 |
61.20 | |
20.00% |
4 / 20 |
|||
highlightPHP | |
0.00% |
0 / 1 |
462 | |
0.00% |
0 / 61 |
1 | <?php declare(strict_types=1); |
2 | |
3 | namespace Aviat\Kilo; |
4 | |
5 | use Aviat\Kilo\Enum\Highlight; |
6 | use Aviat\Kilo\Enum\RawKeyCode; |
7 | |
8 | /** |
9 | * @property-read int $size |
10 | * @property-read int $rsize |
11 | * @property-read string $chars |
12 | */ |
13 | class Row { |
14 | // use Traits\MagicProperties; |
15 | |
16 | /** |
17 | * The version of the row to be displayed (where tabs are converted to display spaces) |
18 | */ |
19 | public string $render = ''; |
20 | |
21 | /** |
22 | * The mapping of characters to their highlighting type |
23 | */ |
24 | public array $hl = []; |
25 | |
26 | /** |
27 | * Are we in the middle of highlighting a multi-line comment? |
28 | */ |
29 | private bool $hlOpenComment = FALSE; |
30 | |
31 | /** |
32 | * Create a row in the current document |
33 | * |
34 | * @param Document $parent |
35 | * @param string $chars |
36 | * @param int $idx |
37 | * @return self |
38 | */ |
39 | public static function new(Document $parent, string $chars, int $idx): self |
40 | { |
41 | return new self( |
42 | $parent, |
43 | $chars, |
44 | $idx, |
45 | ); |
46 | } |
47 | |
48 | /** |
49 | * Create an empty Row |
50 | * |
51 | * @return self |
52 | */ |
53 | public static function default(): self |
54 | { |
55 | return new self( |
56 | Document::new(), |
57 | '', |
58 | 0, |
59 | ); |
60 | } |
61 | |
62 | private function __construct( |
63 | /** |
64 | * The document that this row belongs to |
65 | */ |
66 | private Document $parent, |
67 | |
68 | /** |
69 | * @var string The raw characters in the row |
70 | */ |
71 | private string $chars, |
72 | |
73 | /** |
74 | * @var int The line number of the current row |
75 | */ |
76 | public int $idx, |
77 | ) {} |
78 | |
79 | public function __get(string $name): mixed |
80 | { |
81 | return match ($name) |
82 | { |
83 | 'size' => strlen($this->chars), |
84 | 'rsize' => strlen($this->render), |
85 | 'chars' => $this->chars, |
86 | default => NULL, |
87 | }; |
88 | } |
89 | |
90 | /** |
91 | * Convert the row contents to a string for saving |
92 | * |
93 | * @return string |
94 | */ |
95 | public function __toString(): string |
96 | { |
97 | return $this->chars . "\n"; |
98 | } |
99 | |
100 | /** |
101 | * Set the properties to display for var_dump |
102 | * |
103 | * @return array |
104 | */ |
105 | public function __debugInfo(): array |
106 | { |
107 | return [ |
108 | 'size' => $this->size, |
109 | 'rsize' => $this->rsize, |
110 | 'chars' => $this->chars, |
111 | 'render' => $this->render, |
112 | 'hl' => $this->hl, |
113 | 'hlOpenComment' => $this->hlOpenComment, |
114 | ]; |
115 | } |
116 | |
117 | /** |
118 | * Is this row a valid part of a document? |
119 | * |
120 | * @return bool |
121 | */ |
122 | public function isValid(): bool |
123 | { |
124 | return ! $this->parent->isEmpty(); |
125 | } |
126 | |
127 | /** |
128 | * Insert the string or character $c at index $at |
129 | * |
130 | * @param int $at |
131 | * @param string $c |
132 | */ |
133 | public function insert(int $at, string $c): void |
134 | { |
135 | if ($at < 0 || $at > $this->size) |
136 | { |
137 | $this->append($c); |
138 | return; |
139 | } |
140 | |
141 | // Safely insert into arbitrary position in the existing string |
142 | $this->chars = substr($this->chars, 0, $at) . $c . substr($this->chars, $at); |
143 | $this->update(); |
144 | } |
145 | |
146 | /** |
147 | * Append $s to the current row |
148 | * |
149 | * @param string $s |
150 | */ |
151 | public function append(string $s): void |
152 | { |
153 | $this->chars .= $s; |
154 | $this->update(); |
155 | } |
156 | |
157 | /** |
158 | * Delete the character at the specified index |
159 | * |
160 | * @param int $at |
161 | */ |
162 | public function delete(int $at): void |
163 | { |
164 | if ($at < 0 || $at >= $this->size) |
165 | { |
166 | return; |
167 | } |
168 | |
169 | $this->chars = substr_replace($this->chars, '', $at, 1); |
170 | $this->update(); |
171 | } |
172 | |
173 | public function setChars(string $chars): void |
174 | { |
175 | $this->chars = $chars; |
176 | $this->update(); |
177 | } |
178 | |
179 | /** |
180 | * Convert tabs to spaces for display, and update syntax highlighting |
181 | */ |
182 | public function update(): void |
183 | { |
184 | $this->render = tabs_to_spaces($this->chars); |
185 | $this->highlight(); |
186 | } |
187 | |
188 | // ------------------------------------------------------------------------ |
189 | // ! Syntax Highlighting |
190 | // ------------------------------------------------------------------------ |
191 | |
192 | /** |
193 | * Parse the current file to apply syntax highlighting |
194 | */ |
195 | public function highlight(): void |
196 | { |
197 | $this->hl = array_fill(0, $this->rsize, Highlight::NORMAL); |
198 | |
199 | if ($this->parent->fileType->name === 'PHP') |
200 | { |
201 | $this->highlightPHP(); |
202 | return; |
203 | } |
204 | |
205 | $syntax = $this->parent->fileType->syntax; |
206 | |
207 | $mcs = $syntax->multiLineCommentStart; |
208 | $mce = $syntax->multiLineCommentEnd; |
209 | |
210 | $mcsLen = strlen($mcs); |
211 | $mceLen = strlen($mce); |
212 | |
213 | $inString = ''; |
214 | $inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment); |
215 | |
216 | $i = 0; |
217 | |
218 | while ($i < $this->rsize) |
219 | { |
220 | // Multi-line comments |
221 | if ($syntax->mlComments() && $inString === '') |
222 | { |
223 | if ($inComment) |
224 | { |
225 | $this->hl[$i] = Highlight::ML_COMMENT; |
226 | if (substr($this->render, $i, $mceLen) === $mce) |
227 | { |
228 | array_replace_range($this->hl, $i, $mceLen, Highlight::ML_COMMENT); |
229 | $i += $mceLen; |
230 | $inComment = FALSE; |
231 | continue; |
232 | } |
233 | |
234 | $i++; |
235 | continue; |
236 | } |
237 | |
238 | if (substr($this->render, $i, $mcsLen) === $mcs) |
239 | { |
240 | array_replace_range($this->hl, $i, $mcsLen, Highlight::ML_COMMENT); |
241 | $i += $mcsLen; |
242 | $inComment = TRUE; |
243 | continue; |
244 | } |
245 | } |
246 | |
247 | if ( |
248 | $this->highlightComment($i, $syntax) |
249 | || $this->highlightPrimaryKeywords($i, $syntax) |
250 | || $this->highlightSecondaryKeywords($i, $syntax) |
251 | || $this->highlightCharacter($i, $syntax) |
252 | || $this->highlightString($i, $syntax) |
253 | || $this->highlightNumber($i, $syntax) |
254 | || $this->highlightOperators($i, $syntax) |
255 | || $this->highlightCommonOperators($i) |
256 | || $this->highlightCommonDelimeters($i) |
257 | ) { |
258 | $i++; |
259 | continue; |
260 | } |
261 | |
262 | $i++; |
263 | } |
264 | |
265 | $changed = $this->hlOpenComment !== $inComment; |
266 | $this->hlOpenComment = $inComment; |
267 | if ($changed && $this->idx + 1 < $this->parent->numRows) |
268 | { |
269 | $this->parent->rows[$this->idx + 1]->highlight(); |
270 | } |
271 | } |
272 | |
273 | protected function highlightNumber(int &$i, Syntax $opts): bool |
274 | { |
275 | $char = $this->render[$i]; |
276 | if ($opts->numbers() && is_digit($char)) |
277 | { |
278 | if ($i > 0) |
279 | { |
280 | $prevChar = $this->render[$i - 1]; |
281 | if ( ! is_separator($prevChar)) |
282 | { |
283 | return false; |
284 | } |
285 | } |
286 | |
287 | while (true) |
288 | { |
289 | $this->hl[$i] = Highlight::NUMBER; |
290 | $i++; |
291 | |
292 | if ($i < strlen($this->render)) |
293 | { |
294 | $nextChar = $this->render[$i]; |
295 | if ($nextChar !== '.' && ! is_digit($nextChar)) |
296 | { |
297 | break; |
298 | } |
299 | } |
300 | else |
301 | { |
302 | break; |
303 | } |
304 | } |
305 | |
306 | return true; |
307 | } |
308 | |
309 | return false; |
310 | } |
311 | |
312 | protected function highlightWord(int &$i, array $keywords, int $syntaxType): bool |
313 | { |
314 | if ($i > 0) |
315 | { |
316 | $prevChar = $this->render[$i - 1]; |
317 | if ( ! is_separator($prevChar)) |
318 | { |
319 | return false; |
320 | } |
321 | } |
322 | |
323 | foreach ($keywords as $k) |
324 | { |
325 | $klen = strlen($k); |
326 | $nextCharOffset = $i + $klen; |
327 | $isEndOfLine = $nextCharOffset >= $this->rsize; |
328 | $nextChar = ($isEndOfLine) ? RawKeyCode::NULL : $this->render[$nextCharOffset]; |
329 | |
330 | if (substr($this->render, $i, $klen) === $k && is_separator($nextChar)) |
331 | { |
332 | array_replace_range($this->hl, $i, $klen, $syntaxType); |
333 | $i += $klen - 1; |
334 | |
335 | return true; |
336 | } |
337 | } |
338 | |
339 | return false; |
340 | } |
341 | |
342 | protected function highlightChar(int &$i, array $chars, int $syntaxType): bool |
343 | { |
344 | $char = $this->render[$i]; |
345 | |
346 | if (in_array($char, $chars, TRUE)) |
347 | { |
348 | $this->hl[$i] = $syntaxType; |
349 | $i += 1; |
350 | |
351 | return true; |
352 | } |
353 | |
354 | return false; |
355 | } |
356 | |
357 | protected function highlightPrimaryKeywords(int &$i, Syntax $opts): bool |
358 | { |
359 | return $this->highlightWord($i, $opts->keywords1, Highlight::KEYWORD1); |
360 | } |
361 | |
362 | protected function highlightSecondaryKeywords(int &$i, Syntax $opts): bool |
363 | { |
364 | return $this->highlightWord($i, $opts->keywords2, Highlight::KEYWORD2); |
365 | } |
366 | |
367 | protected function highlightOperators(int &$i, Syntax $opts): bool |
368 | { |
369 | return $this->highlightWord($i, $opts->operators, Highlight::OPERATOR); |
370 | } |
371 | |
372 | protected function highlightCommonOperators(int &$i): bool |
373 | { |
374 | return $this->highlightChar( |
375 | $i, |
376 | ['+', '-', '*', '/', '<', '^', '>', '%', '=', ':', ',', ';', '&', '~'], |
377 | Highlight::OPERATOR |
378 | ); |
379 | } |
380 | |
381 | protected function highlightCommonDelimeters(int &$i): bool |
382 | { |
383 | return $this->highlightChar( |
384 | $i, |
385 | ['{', '}', '[', ']', '(', ')'], |
386 | Highlight::DELIMITER |
387 | ); |
388 | } |
389 | |
390 | protected function highlightCharacter(int &$i, Syntax $opts): bool |
391 | { |
392 | if (($i + 1) >= $this->rsize) |
393 | { |
394 | return false; |
395 | } |
396 | |
397 | $char = $this->render[$i]; |
398 | $nextChar = $this->render[$i + 1]; |
399 | |
400 | if ($opts->characters() && $char === "'") |
401 | { |
402 | $offset = ($nextChar === '\\') ? $i + 2 : $i + 1; |
403 | $closingIndex = strpos($this->render, "'", $offset); |
404 | if ($closingIndex === false) |
405 | { |
406 | return false; |
407 | } |
408 | |
409 | $closingChar = $this->render[$closingIndex]; |
410 | if ($closingChar === "'") |
411 | { |
412 | array_replace_range($this->hl, $i, $closingIndex - $i + 1, Highlight::CHARACTER); |
413 | $i = $closingIndex + 1; |
414 | |
415 | return true; |
416 | } |
417 | } |
418 | |
419 | return false; |
420 | } |
421 | |
422 | protected function highlightComment(int &$i, Syntax $opts): bool |
423 | { |
424 | if ( ! $opts->comments()) |
425 | { |
426 | return false; |
427 | } |
428 | |
429 | $scs = $opts->singleLineCommentStart; |
430 | $scsLen = strlen($scs); |
431 | |
432 | if ($scsLen > 0 && substr($this->render, $i, $scsLen) === $scs) |
433 | { |
434 | array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::COMMENT); |
435 | $i = $this->rsize; |
436 | |
437 | return true; |
438 | } |
439 | |
440 | return false; |
441 | } |
442 | |
443 | protected function highlightString(int &$i, Syntax $opts): bool |
444 | { |
445 | $char = $this->render[$i]; |
446 | |
447 | // If there's a separate character type, highlight that separately |
448 | if ($opts->hasChar() && $char === "'") |
449 | { |
450 | return false; |
451 | } |
452 | |
453 | if ($opts->strings() && $char === '"' || $char === '\'') |
454 | { |
455 | $quote = $char; |
456 | $this->hl[$i] = Highlight::STRING; |
457 | $i++; |
458 | |
459 | while ($i < $this->rsize) |
460 | { |
461 | $char = $this->render[$i]; |
462 | $this->hl[$i] = Highlight::STRING; |
463 | |
464 | // Check for escaped character |
465 | if ($char === '\\' && $i+1 < $this->rsize) |
466 | { |
467 | $this->hl[$i + 1] = Highlight::STRING; |
468 | $i += 2; |
469 | continue; |
470 | } |
471 | |
472 | // End of the string! |
473 | if ($char === $quote) |
474 | { |
475 | $i++; |
476 | break; |
477 | } |
478 | |
479 | $i++; |
480 | } |
481 | |
482 | return true; |
483 | } |
484 | |
485 | return false; |
486 | } |
487 | |
488 | protected function highlightPHP(): void |
489 | { |
490 | $rowNum = $this->idx + 1; |
491 | |
492 | $hasRowTokens = array_key_exists($rowNum, $this->parent->tokens); |
493 | |
494 | if ( ! ( |
495 | $hasRowTokens && |
496 | $this->idx < $this->parent->numRows |
497 | )) |
498 | { |
499 | return; |
500 | } |
501 | |
502 | $tokens = $this->parent->tokens[$rowNum]; |
503 | |
504 | $inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment); |
505 | |
506 | // Keep track of where you are in the line, so that |
507 | // multiples of the same tokens can be effectively matched |
508 | $offset = 0; |
509 | |
510 | foreach ($tokens as $token) |
511 | { |
512 | if ($offset >= $this->rsize) |
513 | { |
514 | break; |
515 | } |
516 | |
517 | // A multi-line comment can end in the middle of a line... |
518 | if ($inComment) |
519 | { |
520 | // Try looking for the end of the comment first |
521 | $commentEnd = strpos($this->render, '*/'); |
522 | if ($commentEnd !== FALSE) |
523 | { |
524 | $inComment = FALSE; |
525 | array_replace_range($this->hl, 0, $commentEnd + 2, Highlight::ML_COMMENT); |
526 | $offset = $commentEnd; |
527 | continue; |
528 | } |
529 | |
530 | // Otherwise, just set the whole row |
531 | $this->hl = array_fill(0, $this->rsize, Highlight::ML_COMMENT); |
532 | $this->hl[$offset] = Highlight::ML_COMMENT; |
533 | break; |
534 | } |
535 | |
536 | $char = $token['char']; |
537 | $charLen = strlen($char); |
538 | if ($charLen === 0 || $offset >= $this->rsize) |
539 | { |
540 | continue; |
541 | } |
542 | $charStart = strpos($this->render, $char, $offset); |
543 | if ($charStart === FALSE) |
544 | { |
545 | continue; |
546 | } |
547 | $charEnd = $charStart + $charLen; |
548 | |
549 | // Start of multiline comment/single line comment |
550 | if (in_array($token['type'], [T_DOC_COMMENT, T_COMMENT], TRUE)) |
551 | { |
552 | // Single line comments |
553 | if (str_has($char, '//') || str_has($char, '#')) |
554 | { |
555 | array_replace_range($this->hl, $charStart, $charLen, Highlight::COMMENT); |
556 | break; |
557 | } |
558 | |
559 | // Start of multi-line comment |
560 | $start = strpos($this->render, '/*', $offset); |
561 | $end = strpos($this->render, '*/', $offset); |
562 | $hasStart = $start !== FALSE; |
563 | $hasEnd = $end !== FALSE; |
564 | |
565 | if ($hasStart) |
566 | { |
567 | if ($hasEnd) |
568 | { |
569 | $len = $end - $start + 2; |
570 | array_replace_range($this->hl, $start, $len, Highlight::ML_COMMENT); |
571 | $inComment = FALSE; |
572 | } |
573 | else |
574 | { |
575 | $inComment = TRUE; |
576 | array_replace_range($this->hl, $start, $charLen - $offset, Highlight::ML_COMMENT); |
577 | $offset = $start + $charLen - $offset; |
578 | } |
579 | } |
580 | |
581 | if ($inComment) |
582 | { |
583 | break; |
584 | } |
585 | } |
586 | |
587 | $tokenHighlight = Highlight::fromPHPToken($token['type']); |
588 | $charHighlight = Highlight::fromPHPChar(trim($token['char'])); |
589 | |
590 | $highlight = match(true) { |
591 | // Matches a predefined PHP token |
592 | $token['type'] !== T_RAW && $tokenHighlight !== Highlight::NORMAL |
593 | => $tokenHighlight, |
594 | |
595 | // Matches a specific syntax character |
596 | $charHighlight !== Highlight::NORMAL => $charHighlight, |
597 | |
598 | default => Highlight::NORMAL, |
599 | }; |
600 | |
601 | if ($highlight !== Highlight::NORMAL) |
602 | { |
603 | array_replace_range($this->hl, $charStart, $charLen, $highlight); |
604 | $offset = $charEnd; |
605 | } |
606 | } |
607 | |
608 | $changed = $this->hlOpenComment !== $inComment; |
609 | $this->hlOpenComment = $inComment; |
610 | if ($changed && ($this->idx + 1) < $this->parent->numRows) |
611 | { |
612 | $this->parent->rows[$this->idx + 1]->highlight(); |
613 | } |
614 | } |
615 | } |