Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
60.00% covered (warning)
60.00%
15 / 25
CRAP
38.96% covered (danger)
38.96%
90 / 231
Row
0.00% covered (danger)
0.00%
0 / 1
60.00% covered (warning)
60.00%
15 / 25
2420.87
38.96% covered (danger)
38.96%
90 / 231
 new
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 default
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 4
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 __get
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
5 / 5
 __toString
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 __debugInfo
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
6 / 6
 isValid
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 insert
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
5 / 5
 append
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 delete
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
4 / 4
 setChars
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 update
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 highlight
0.00% covered (danger)
0.00%
0 / 1
52.21
56.82% covered (warning)
56.82%
25 / 44
 highlightNumber
0.00% covered (danger)
0.00%
0 / 1
52.45
18.75% covered (danger)
18.75%
3 / 16
 highlightWord
0.00% covered (danger)
0.00%
0 / 1
16.14
42.86% covered (danger)
42.86%
6 / 14
 highlightChar
0.00% covered (danger)
0.00%
0 / 1
2.50
50.00% covered (danger)
50.00%
3 / 6
 highlightPrimaryKeywords
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 highlightSecondaryKeywords
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 highlightOperators
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 highlightCommonOperators
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 highlightCommonDelimeters
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 highlightCharacter
0.00% covered (danger)
0.00%
0 / 1
17.58
40.00% covered (danger)
40.00%
6 / 15
 highlightComment
0.00% covered (danger)
0.00%
0 / 1
11.53
22.22% covered (danger)
22.22%
2 / 9
 highlightString
0.00% covered (danger)
0.00%
0 / 1
61.20
20.00% covered (danger)
20.00%
4 / 20
 highlightPHP
0.00% covered (danger)
0.00%
0 / 1
462
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\RawKeyCode;
7
8/**
9 * @property-read int $size
10 * @property-read int $rsize
11 * @property-read string $chars
12 */
13class 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}