Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 169
Row
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 11
5402
0.00% covered (danger)
0.00%
0 / 169
 new
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 5
 __construct
n/a
0 / 0
1
n/a
0 / 0
 __get
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 __set
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 __toString
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 __debugInfo
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 insertChar
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 6
 appendString
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 deleteChar
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 5
 update
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 updateSyntax
0.00% covered (danger)
0.00%
0 / 1
1332
0.00% covered (danger)
0.00%
0 / 81
 updateSyntaxPHP
0.00% covered (danger)
0.00%
0 / 1
506
0.00% covered (danger)
0.00%
0 / 61
1<?php declare(strict_types=1);
2
3namespace Aviat\Kilo;
4
5use Aviat\Kilo\Enum\Highlight;
6use Aviat\Kilo\Enum\KeyCode;
7
8/**
9 * @property-read int size
10 * @property-read int rsize
11 */
12class Row {
13    use Traits\MagicProperties;
14
15    private string $chars = '';
16    public string $render = '';
17
18    public array $hl = [];
19
20    public int $idx;
21
22    // This feels dirty...
23    private Editor $parent;
24    private bool $hlOpenComment = FALSE;
25
26    private const T_RAW = -1;
27
28    private array $phpTokenHighlightMap = [
29        // Delimiters
30        T_ARRAY => Highlight::DELIMITER,
31        T_CURLY_OPEN => Highlight::DELIMITER,
32        T_DOLLAR_OPEN_CURLY_BRACES => Highlight::DELIMITER,
33        T_OPEN_TAG => Highlight::DELIMITER,
34        T_OPEN_TAG_WITH_ECHO => Highlight::DELIMITER,
35        T_CLOSE_TAG => Highlight::DELIMITER,
36        T_START_HEREDOC => Highlight::DELIMITER,
37        T_END_HEREDOC => Highlight::DELIMITER,
38
39        // Number literals and magic constants
40        T_DIR => Highlight::NUMBER,
41        T_TRAIT_C => Highlight::NUMBER,
42        T_DNUMBER => Highlight::NUMBER,
43        T_LNUMBER => Highlight::NUMBER,
44
45        // String literals
46        T_CONSTANT_ENCAPSED_STRING => Highlight::STRING,
47        T_ENCAPSED_AND_WHITESPACE => Highlight::STRING,
48
49        // Simple variables
50        T_VARIABLE => Highlight::VARIABLE,
51        T_STRING_VARNAME => Highlight::VARIABLE,
52
53        // Operators
54        T_AS => Highlight::OPERATOR,
55        T_AND_EQUAL => Highlight::OPERATOR,
56        T_BOOLEAN_AND => Highlight::OPERATOR,
57        T_BOOLEAN_OR => Highlight::OPERATOR,
58        T_COALESCE => Highlight::OPERATOR,
59        T_CONCAT_EQUAL => Highlight::OPERATOR,
60        T_DEC => Highlight::OPERATOR,
61        T_DIV_EQUAL => Highlight::OPERATOR,
62        T_DOUBLE_ARROW => Highlight::OPERATOR,
63        T_DOUBLE_COLON => Highlight::OPERATOR,
64        T_ELLIPSIS => Highlight::OPERATOR,
65        T_INC => Highlight::OPERATOR,
66        T_IS_EQUAL => Highlight::OPERATOR,
67        T_IS_GREATER_OR_EQUAL => Highlight::OPERATOR,
68        T_IS_IDENTICAL => Highlight::OPERATOR,
69        T_IS_NOT_EQUAL => Highlight::OPERATOR,
70        T_IS_NOT_IDENTICAL => Highlight::OPERATOR,
71        T_IS_SMALLER_OR_EQUAL => Highlight::OPERATOR,
72        T_SPACESHIP => Highlight::OPERATOR,
73        T_LOGICAL_AND => Highlight::OPERATOR,
74        T_LOGICAL_OR => Highlight::OPERATOR,
75        T_LOGICAL_XOR => Highlight::OPERATOR,
76        T_MINUS_EQUAL => Highlight::OPERATOR,
77        T_MOD_EQUAL => Highlight::OPERATOR,
78        T_MUL_EQUAL => Highlight::OPERATOR,
79        T_NS_SEPARATOR => Highlight::OPERATOR,
80        T_NULLSAFE_OBJECT_OPERATOR => Highlight::OPERATOR,
81        T_OBJECT_OPERATOR => Highlight::OPERATOR,
82        T_OR_EQUAL => Highlight::OPERATOR,
83        T_PLUS_EQUAL => Highlight::OPERATOR,
84        T_POW => Highlight::OPERATOR,
85        T_POW_EQUAL => Highlight::OPERATOR,
86        T_SL => Highlight::OPERATOR,
87        T_SL_EQUAL => Highlight::OPERATOR,
88        T_SR => Highlight::OPERATOR,
89        T_SR_EQUAL => Highlight::OPERATOR,
90        T_XOR_EQUAL => Highlight::OPERATOR,
91
92        // Keywords1
93        T_ABSTRACT => Highlight::KEYWORD1,
94        T_AS => Highlight::KEYWORD1,
95        T_BREAK => Highlight::KEYWORD1,
96        T_CASE => Highlight::KEYWORD1,
97        T_CATCH => Highlight::KEYWORD1,
98        T_CLASS => Highlight::KEYWORD1,
99        T_CLONE => Highlight::KEYWORD1,
100        T_CONST => Highlight::KEYWORD1,
101        T_CONTINUE => Highlight::KEYWORD1,
102        T_DECLARE => Highlight::KEYWORD1,
103        T_DEFAULT => Highlight::KEYWORD1,
104        T_DO => Highlight::KEYWORD1,
105        T_ELSE => Highlight::KEYWORD1,
106        T_ELSEIF => Highlight::KEYWORD1,
107        T_ENDDECLARE => Highlight::KEYWORD1,
108        T_ENDFOR => Highlight::KEYWORD1,
109        T_ENDFOREACH => Highlight::KEYWORD1,
110        T_ENDIF => Highlight::KEYWORD1,
111        T_ENDSWITCH => Highlight::KEYWORD1,
112        T_ENDWHILE => Highlight::KEYWORD1,
113        T_EXIT => Highlight::KEYWORD1,
114        T_EXTENDS => Highlight::KEYWORD1,
115        T_FINAL => Highlight::KEYWORD1,
116        T_FINALLY => Highlight::KEYWORD1,
117        T_FN => Highlight::KEYWORD1,
118        T_FOR => Highlight::KEYWORD1,
119        T_FOREACH => Highlight::KEYWORD1,
120        T_FUNCTION => Highlight::KEYWORD1,
121        T_GLOBAL => Highlight::KEYWORD1,
122        T_GOTO => Highlight::KEYWORD1,
123        T_HALT_COMPILER => Highlight::KEYWORD1,
124        T_IF => Highlight::KEYWORD1,
125        T_IMPLEMENTS => Highlight::KEYWORD1,
126        T_INSTANCEOF => Highlight::KEYWORD1,
127        T_INSTEADOF => Highlight::KEYWORD1,
128        T_INTERFACE => Highlight::KEYWORD1,
129        T_NAMESPACE => Highlight::KEYWORD1,
130        T_MATCH => Highlight::KEYWORD1,
131        T_NEW => Highlight::KEYWORD1,
132        T_PRIVATE => Highlight::KEYWORD1,
133        T_PUBLIC => Highlight::KEYWORD1,
134        T_PROTECTED => Highlight::KEYWORD1,
135        T_RETURN => Highlight::KEYWORD1,
136        T_STATIC => Highlight::KEYWORD1,
137        T_SWITCH => Highlight::KEYWORD1,
138        T_THROW => Highlight::KEYWORD1,
139        T_TRAIT => Highlight::KEYWORD1,
140        T_TRY => Highlight::KEYWORD1,
141        T_USE => Highlight::KEYWORD1,
142        T_VAR => Highlight::KEYWORD1,
143        T_WHILE => Highlight::KEYWORD1,
144        T_YIELD => Highlight::KEYWORD1,
145        T_YIELD_FROM => Highlight::KEYWORD1,
146
147        // Not string literals, but identifiers, keywords, etc.
148        // T_STRING => Highlight::KEYWORD2,
149
150        // Types and casts
151        T_ARRAY_CAST => Highlight::KEYWORD2,
152        T_BOOL_CAST => Highlight::KEYWORD2,
153        T_CALLABLE => Highlight::KEYWORD2,
154        T_DOUBLE_CAST => Highlight::KEYWORD2,
155        T_INT_CAST => Highlight::KEYWORD2,
156        T_OBJECT_CAST => Highlight::KEYWORD2,
157        T_STRING_CAST => Highlight::KEYWORD2,
158        T_UNSET_CAST => Highlight::KEYWORD2,
159
160        // Invalid syntax
161        T_BAD_CHARACTER => Highlight::INVALID,
162    ];
163
164    private array $phpCharacterHighlightMap = [
165        // Delimiter characters
166        '[' => Highlight::DELIMITER,
167        ']' => Highlight::DELIMITER,
168        '{' => Highlight::DELIMITER,
169        '}' => Highlight::DELIMITER,
170        '(' => Highlight::DELIMITER,
171        ')' => Highlight::DELIMITER,
172        '"' => Highlight::DELIMITER,
173        "'" => Highlight::DELIMITER,
174
175        // Single character operators
176        '?' => Highlight::OPERATOR,
177        ',' => Highlight::OPERATOR,
178        ';' => Highlight::OPERATOR,
179        ':' => Highlight::OPERATOR,
180        '^' => Highlight::OPERATOR,
181        '%' => Highlight::OPERATOR,
182        '+' => Highlight::OPERATOR,
183        '-' => Highlight::OPERATOR,
184        '*' => Highlight::OPERATOR,
185        '/' => Highlight::OPERATOR,
186        '.' => Highlight::OPERATOR,
187        '|' => Highlight::OPERATOR,
188        '~' => Highlight::OPERATOR,
189        '>' => Highlight::OPERATOR,
190        '<' => Highlight::OPERATOR,
191        '=' => Highlight::OPERATOR,
192        '!' => Highlight::OPERATOR,
193    ];
194
195    public static function new(Editor $parent, string $chars, int $idx): self
196    {
197        $self = new self();
198        $self->chars = $chars;
199        $self->parent = $parent;
200        $self->idx = $idx;
201
202        return $self;
203    }
204
205    private function __construct() {}
206
207    public function __get(string $name)
208    {
209        return match ($name)
210        {
211            'size' => strlen($this->chars),
212            'rsize' => strlen($this->render),
213            'chars' => $this->chars,
214            default => NULL,
215        };
216    }
217
218    public function __set(string $key, mixed $value): void
219    {
220        if ($key === 'chars')
221        {
222            $this->chars = $value;
223            $this->update();
224        }
225    }
226
227    public function __toString(): string
228    {
229        return $this->chars . "\n";
230    }
231
232    public function __debugInfo(): array
233    {
234        return [
235            'size' => $this->size,
236            'rsize' => $this->rsize,
237            'chars' => $this->chars,
238            'render' => $this->render,
239            'hl' => $this->hl,
240            'hlOpenComment' => $this->hlOpenComment,
241        ];
242    }
243
244    public function insertChar(int $at, string $c): void
245    {
246        if ($at < 0 || $at > $this->size)
247        {
248            $this->appendString($c);
249            return;
250        }
251
252        // Safely insert into arbitrary position in the existing string
253        $this->chars = substr($this->chars, 0, $at) . $c . substr($this->chars, $at);
254        $this->update();
255
256        $this->parent->dirty++;
257    }
258
259    public function appendString(string $s): void
260    {
261        $this->chars .= $s;
262        $this->update();
263
264        $this->parent->dirty++;
265    }
266
267    public function deleteChar(int $at): void
268    {
269        if ($at < 0 || $at >= $this->size)
270        {
271            return;
272        }
273
274        $this->chars = substr_replace($this->chars, '', $at, 1);
275        $this->update();
276
277        $this->parent->dirty++;
278    }
279
280    public function update(): void
281    {
282        $this->render = tabs_to_spaces($this->chars);
283
284        $this->updateSyntax();
285    }
286
287    // ------------------------------------------------------------------------
288    // ! Syntax Highlighting
289    // ------------------------------------------------------------------------
290
291    public function updateSyntax(): void
292    {
293        $this->hl = array_fill(0, $this->rsize, Highlight::NORMAL);
294
295        if ($this->parent->syntax === NULL)
296        {
297            return;
298        }
299
300        if ($this->parent->syntax->filetype === 'PHP')
301        {
302            $this->updateSyntaxPHP();
303            return;
304        }
305
306        $keywords1 = $this->parent->syntax->keywords1;
307        $keywords2 = $this->parent->syntax->keywords2;
308
309        $scs = $this->parent->syntax->singleLineCommentStart;
310        $mcs = $this->parent->syntax->multiLineCommentStart;
311        $mce = $this->parent->syntax->multiLineCommentEnd;
312
313        $scsLen = strlen($scs);
314        $mcsLen = strlen($mcs);
315        $mceLen = strlen($mce);
316
317        $prevSep = TRUE;
318        $inString = '';
319        $inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment);
320
321        $i = 0;
322
323        while ($i < $this->rsize)
324        {
325            $char = $this->render[$i];
326            $prevHl = ($i > 0) ? $this->hl[$i - 1] : Highlight::NORMAL;
327
328            // Single-line comments
329            if ($scsLen > 0 && $inString === '' && $inComment === FALSE
330                && substr($this->render, $i, $scsLen) === $scs)
331            {
332                array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::COMMENT);
333                break;
334            }
335
336            // Multi-line comments
337            if ($mcsLen > 0 && $mceLen > 0 && $inString === '')
338            {
339                if ($inComment)
340                {
341                    $this->hl[$i] = Highlight::ML_COMMENT;
342                    if (substr($this->render, $i, $mceLen) === $mce)
343                    {
344                        array_replace_range($this->hl, $i, $mceLen, Highlight::ML_COMMENT);
345                        $i += $mceLen;
346                        $inComment = FALSE;
347                        $prevSep = TRUE;
348                        continue;
349                    }
350
351                    $i++;
352                    continue;
353                }
354
355                if (substr($this->render, $i, $mcsLen) === $mcs)
356                {
357                    array_replace_range($this->hl, $i, $mcsLen, Highlight::ML_COMMENT);
358                    $i += $mcsLen;
359                    $inComment = TRUE;
360                    continue;
361                }
362            }
363
364            // String/Char literals
365            if ($this->parent->syntax->flags & Syntax::HIGHLIGHT_STRINGS)
366            {
367                if ($inString !== '')
368                {
369                    $this->hl[$i] = Highlight::STRING;
370
371                    // Check for escaped character
372                    if ($char === '\\' && $i+1 < $this->rsize)
373                    {
374                        $this->hl[$i + 1] = Highlight::STRING;
375                        $i += 2;
376                        continue;
377                    }
378
379                    if ($char === $inString)
380                    {
381                        $inString = '';
382                    }
383                    $i++;
384                    $prevSep = 1;
385                    continue;
386                }
387
388                if ( $char === '"' || $char === '\'')
389                {
390                    $inString = $char;
391                    $this->hl[$i] = Highlight::STRING;
392                    $i++;
393                    continue;
394                }
395            }
396
397            // Numbers, including decimal points
398            if ($this->parent->syntax->flags & Syntax::HIGHLIGHT_NUMBERS)
399            {
400                if (
401                    ($char === '.' && $prevHl === Highlight::NUMBER) ||
402                    (($prevSep || $prevHl === Highlight::NUMBER) && is_digit($char))
403                )
404                {
405                    $this->hl[$i] = Highlight::NUMBER;
406                    $i++;
407                    $prevSep = FALSE;
408                    continue;
409                }
410            }
411
412            // Keywords
413            if ($prevSep)
414            {
415                $findKeywords = function (array $keywords, int $syntaxType) use (&$i): void
416                {
417                    foreach ($keywords as $k)
418                    {
419                        $klen = strlen($k);
420                        $nextCharOffset = $i + $klen;
421                        $isEndOfLine = $nextCharOffset >= $this->rsize;
422                        $nextChar = ($isEndOfLine) ? KeyCode::NULL : $this->render[$nextCharOffset];
423
424                        if (substr($this->render, $i, $klen) === $k && is_separator($nextChar))
425                        {
426                            array_replace_range($this->hl, $i, $klen, $syntaxType);
427                            $i += $klen - 1;
428                            break;
429                        }
430                    }
431                };
432
433                $findKeywords($keywords1, Highlight::KEYWORD1);
434                $findKeywords($keywords2, Highlight::KEYWORD2);
435            }
436
437            $prevSep = is_separator($char);
438            $i++;
439        }
440
441        $changed = $this->hlOpenComment !== $inComment;
442        $this->hlOpenComment = $inComment;
443        if ($changed && $this->idx + 1 < $this->parent->numRows)
444        {
445            // @codeCoverageIgnoreStart
446            $this->parent->rows[$this->idx + 1]->updateSyntax();
447            // @codeCoverageIgnoreEnd
448        }
449    }
450
451    protected function updateSyntaxPHP():void
452    {
453        $rowNum = $this->idx + 1;
454
455        $hasRowTokens = array_key_exists($rowNum, $this->parent->tokens);
456
457        if ( ! (
458            $hasRowTokens &&
459            $this->idx < $this->parent->numRows
460        ))
461        {
462            // @codeCoverageIgnoreStart
463            return;
464            // @codeCoverageIgnoreEnd
465        }
466
467        $tokens = $this->parent->tokens[$rowNum];
468
469        $inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment);
470
471        // Keep track of where you are in the line, so that
472        // multiples of the same tokens can be effectively matched
473        $offset = 0;
474
475        foreach ($tokens as $token)
476        {
477            if ($offset >= $this->rsize)
478            {
479                // @codeCoverageIgnoreStart
480                break;
481                // @codeCoverageIgnoreEnd
482            }
483
484            // A multi-line comment can end in the middle of a line...
485            if ($inComment)
486            {
487                // Try looking for the end of the comment first
488                $commentEnd = strpos($this->render, '*/');
489                if ($commentEnd !== FALSE)
490                {
491                    $inComment = FALSE;
492                    array_replace_range($this->hl, 0, $commentEnd + 2, Highlight::ML_COMMENT);
493                    $offset = $commentEnd;
494                    continue;
495                }
496
497                // Otherwise, just set the whole row
498                $this->hl = array_fill(0, $this->rsize, Highlight::ML_COMMENT);
499                $this->hl[$offset] = Highlight::ML_COMMENT;
500                break;
501            }
502
503            $char = $token['char']; // ?? '';
504            $charLen = strlen($char);
505            if ($charLen === 0 || $offset >= $this->rsize)
506            {
507                // @codeCoverageIgnoreStart
508                continue;
509                // @codeCoverageIgnoreEnd
510            }
511            $charStart = strpos($this->render, $char, $offset);
512            if ($charStart === FALSE)
513            {
514                continue;
515            }
516            $charEnd = $charStart + $charLen;
517
518            // Start of multiline comment/single line comment
519            if (in_array($token['type'], [T_DOC_COMMENT, T_COMMENT], TRUE))
520            {
521                // Single line comments
522                if (str_contains($char, '//') || str_contains($char, '#'))
523                {
524                    array_replace_range($this->hl, $charStart, $charLen, Highlight::COMMENT);
525                    break;
526                }
527
528                // Start of multi-line comment
529                $start = strpos($this->render, '/*', $offset);
530                $end = strpos($this->render, '*/', $offset);
531                $hasStart = $start !== FALSE;
532                $hasEnd = $end !== FALSE;
533
534                if ($hasStart)
535                {
536                    if ($hasEnd)
537                    {
538                        $len = $end - $start + 2;
539                        array_replace_range($this->hl, $start, $len, Highlight::ML_COMMENT);
540                        $inComment = FALSE;
541                    }
542                    else
543                    {
544                        $inComment = TRUE;
545                        array_replace_range($this->hl, $start, $charLen - $offset, Highlight::ML_COMMENT);
546                        $offset = $start + $charLen - $offset;
547                    }
548                }
549
550                if ($inComment)
551                {
552                    break;
553                }
554            }
555
556            if (array_key_exists($token['type'], $this->phpTokenHighlightMap))
557            {
558                $hl = $this->phpTokenHighlightMap[$token['type']];
559                array_replace_range($this->hl, $charStart, $charLen, $hl);
560                $offset = $charEnd;
561                continue;
562            }
563
564            // Types/identifiers/keywords that don't have their own token
565            if (in_array($token['char'], $this->parent->syntax->keywords2, TRUE))
566            {
567                array_replace_range($this->hl, $charStart, $charLen, Highlight::KEYWORD2);
568                $offset = $charEnd;
569                continue;
570            }
571
572            // Highlight raw characters
573            if (array_key_exists(trim($token['char']), $this->phpCharacterHighlightMap))
574            {
575                $hl = $this->phpCharacterHighlightMap[trim($token['char'])];
576                array_replace_range($this->hl, $charStart, $charLen, $hl);
577                $offset = $charEnd;
578                continue;
579            }
580        }
581
582        $changed = $this->hlOpenComment !== $inComment;
583        $this->hlOpenComment = $inComment;
584        if ($changed && $this->idx + 1 < $this->parent->numRows)
585        {
586            // @codeCoverageIgnoreStart
587            $this->parent->rows[$this->idx + 1]->updateSyntax();
588            // @codeCoverageIgnoreEnd
589        }
590    }
591}