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 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 319
Editor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 23
19182
0.00% covered (danger)
0.00%
0 / 319
 new
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 3
 __construct
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 11
 __debugInfo
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 6
 run
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 setStatusMessage
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 rowCxToRx
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 6
 rowRxToCx
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 8
 save
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 11
 findCallback
0.00% covered (danger)
0.00%
0 / 1
182
0.00% covered (danger)
0.00%
0 / 42
 find
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 scroll
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 13
 drawRows
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 7
 drawRow
0.00% covered (danger)
0.00%
0 / 1
132
0.00% covered (danger)
0.00%
0 / 36
 drawPlaceholderRow
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 13
 drawStatusBar
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 19
 drawMessageBar
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 6
 refreshScreen
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 10
 prompt
0.00% covered (danger)
0.00%
0 / 1
182
0.00% covered (danger)
0.00%
0 / 18
 processKeypress
0.00% covered (danger)
0.00%
0 / 1
420
0.00% covered (danger)
0.00%
0 / 34
 moveCursor
0.00% covered (danger)
0.00%
0 / 1
420
0.00% covered (danger)
0.00%
0 / 46
 insertChar
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 removeChar
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 5
 quitAttempt
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 10
1<?php declare(strict_types=1);
2
3namespace Aviat\Kilo;
4
5use Aviat\Kilo\Type\TerminalSize;
6use Aviat\Kilo\Enum\{
7    Color,
8    RawKeyCode,
9    KeyType,
10    Highlight,
11    SearchDirection
12};
13use Aviat\Kilo\Type\{Point, StatusMessage};
14
15/**
16 * // Don't highlight this!
17 */
18class Editor {
19    /**
20     * @var string The screen buffer
21     */
22    private string $outputBuffer = '';
23
24    /**
25     * @var Point The 0-based location of the cursor in the current viewport
26     */
27    protected Point $cursor;
28
29    /**
30     * @var Point The scroll offset of the file in the current viewport
31     */
32    protected Point $offset;
33
34    /**
35     * @var Document The document being edited
36     */
37    protected Document $document;
38
39    /**
40     * @var StatusMessage A disappearing status message
41     */
42    protected StatusMessage $statusMessage;
43
44    /**
45     * @var TerminalSize The size of the terminal in rows and columns
46     */
47    protected TerminalSize $terminalSize;
48
49    /**
50     * @var int The rendered cursor position
51     */
52    protected int $renderX = 0;
53
54    /**
55     * @var bool Should we stop the rendering loop?
56     */
57    protected bool $shouldQuit = false;
58
59    /**
60     * @var int The number of times to confirm you wish to quit
61     */
62    protected int $quitTimes = KILO_QUIT_TIMES;
63
64    /**
65     * Create the Editor instance with CLI arguments
66     *
67     * @param int $argc
68     * @param array $argv
69     * @return Editor
70     */
71    public static function new(int $argc = 0, array $argv = []): Editor
72    {
73        if ($argc >= 2 && ! empty($argv[1]))
74        {
75            return new self($argv[1]);
76        }
77
78        return new self();
79    }
80
81    /**
82     * The real constructor, ladies and gentlemen
83     *
84     * @param string|null $filename
85     */
86    private function __construct(?string $filename = NULL)
87    {
88        $this->statusMessage = StatusMessage::from('HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find');
89        $this->cursor = Point::new();
90        $this->offset = Point::new();
91        $this->terminalSize = Terminal::size();
92
93        if (is_string($filename))
94        {
95            $maybeDocument = Document::new()->open($filename);
96            if ($maybeDocument === NULL)
97            {
98                $this->document = Document::new();
99                $this->setStatusMessage("ERR: Could not open file: {}", $filename);
100            }
101            else
102            {
103                $this->document = $maybeDocument;
104            }
105        }
106        else
107        {
108            $this->document = Document::new();
109        }
110    }
111
112    public function __debugInfo(): array
113    {
114        return [
115            'cursor' => $this->cursor,
116            'document' => $this->document,
117            'offset' => $this->offset,
118            'renderX' => $this->renderX,
119            'terminalSize' => $this->terminalSize,
120            'statusMessage' => $this->statusMessage,
121        ];
122    }
123
124    /**
125     * Start the input loop
126     */
127    public function run(): void
128    {
129        while ( ! $this->shouldQuit)
130        {
131            $this->refreshScreen();
132            $this->processKeypress();
133        }
134    }
135
136    /**
137     * Set a status message to be displayed, using printf formatting
138     * @param string $fmt
139     * @param mixed ...$args
140     */
141    public function setStatusMessage(string $fmt, mixed ...$args): void
142    {
143        $text = func_num_args() > 1
144            ? sprintf($fmt, ...$args)
145            : $fmt;
146
147        $this->statusMessage = StatusMessage::from($text);
148    }
149
150    // ------------------------------------------------------------------------
151    // ! Row Operations
152    // ------------------------------------------------------------------------
153
154    /**
155     * Cursor X to Render X
156     *
157     * @param Row $row
158     * @param int $cx
159     * @return int
160     */
161    protected function rowCxToRx(Row $row, int $cx): int
162    {
163        $rx = 0;
164        for ($i = 0; $i < $cx; $i++)
165        {
166            if ($row->chars[$i] === RawKeyCode::TAB)
167            {
168                $rx += (KILO_TAB_STOP - 1) - ($rx % KILO_TAB_STOP);
169            }
170            $rx++;
171        }
172
173        return $rx;
174    }
175
176    /**
177     * Render X to Cursor X
178     *
179     * @param Row $row
180     * @param int $rx
181     * @return int
182     */
183    protected function rowRxToCx(Row $row, int $rx): int
184    {
185        $cur_rx = 0;
186        for ($cx = 0; $cx < $row->size; $cx++)
187        {
188            if ($row->chars[$cx] === RawKeyCode::TAB)
189            {
190                $cur_rx += (KILO_TAB_STOP - 1) - ($cur_rx % KILO_TAB_STOP);
191            }
192            $cur_rx++;
193
194            if ($cur_rx > $rx)
195            {
196                return $cx;
197            }
198        }
199
200        return $cx;
201    }
202
203    // ------------------------------------------------------------------------
204    // ! File I/O
205    // ------------------------------------------------------------------------
206
207    protected function save(): void
208    {
209        if ($this->document->filename === '')
210        {
211            $newFilename = $this->prompt('Save as: %s');
212            if ($newFilename === '')
213            {
214                $this->setStatusMessage('Save aborted');
215                return;
216            }
217
218            $this->document->filename = $newFilename;
219        }
220
221        $res = $this->document->save();
222
223        if ($res !== FALSE)
224        {
225            $this->setStatusMessage('%d bytes written to disk', $res);
226            return;
227        }
228
229        $this->setStatusMessage('Failed to save! I/O error: %s', error_get_last()['message'] ?? '');
230    }
231
232    // ------------------------------------------------------------------------
233    // ! Find
234    // ------------------------------------------------------------------------
235
236    protected function findCallback(string $query, string $key): void
237    {
238        static $lastMatch = NO_MATCH;
239        static $direction = SearchDirection::FORWARD;
240
241        static $savedHlLine = 0;
242        static $savedHl = [];
243
244        if ( ! empty($savedHl))
245        {
246            $row = $this->document->row($savedHlLine);
247
248            if ($row->isValid())
249            {
250                $row->hl = $savedHl;
251            }
252
253            $savedHl = [];
254        }
255
256        $direction = match ($key) {
257            KeyType::ARROW_UP, KeyType::ARROW_LEFT => SearchDirection::BACKWARD,
258            default => SearchDirection::FORWARD
259        };
260
261        $arrowKeys = [KeyType::ARROW_UP, KeyType::ARROW_DOWN, KeyType::ARROW_LEFT, KeyType::ARROW_RIGHT];
262
263        // Reset search state with non arrow-key input
264        if ( ! in_array($key, $arrowKeys, true))
265        {
266            $lastMatch = NO_MATCH;
267            $direction = SearchDirection::FORWARD;
268
269            if ($key === RawKeyCode::ENTER || $key === RawKeyCode::ESCAPE)
270            {
271                return;
272            }
273        }
274
275        if ($lastMatch === NO_MATCH)
276        {
277            $direction = SearchDirection::FORWARD;
278        }
279
280        $current = (int)$lastMatch;
281
282        if (empty($query))
283        {
284            return;
285        }
286
287        for ($i = 0; $i < $this->document->numRows; $i++)
288        {
289            $current += $direction;
290            if ($current === -1)
291            {
292                $current = $this->document->numRows - 1;
293            }
294            else if ($current === $this->document->numRows)
295            {
296                $current = 0;
297            }
298
299            $row = $this->document->row($current);
300            if ( ! $row->isValid())
301            {
302                break;
303            }
304
305            $match = strpos($row->render, $query);
306            if ($match !== FALSE)
307            {
308                $lastMatch = $current;
309                $this->cursor->y = (int)$current;
310                $this->cursor->x = $this->rowRxToCx($row, $match);
311                $this->offset->y = $this->document->numRows;
312
313                $savedHlLine = $current;
314                $savedHl = $row->hl;
315                // Update the highlight array of the relevant row with the 'MATCH' type
316                array_replace_range($row->hl, $match, strlen($query), Highlight::MATCH);
317
318                break;
319            }
320        }
321    }
322
323    protected function find(): void
324    {
325        $savedCursor = Point::from($this->cursor);
326        $savedOffset = Point::from($this->offset);
327
328        $query = $this->prompt('Search: %s (Use ESC/Arrows/Enter)', [$this, 'findCallback']);
329
330        // If they pressed escape, the query will be empty,
331        // restore original cursor and scroll locations
332        if ($query === '')
333        {
334            $this->cursor = Point::from($savedCursor);
335            $this->offset = Point::from($savedOffset);
336        }
337    }
338
339    // ------------------------------------------------------------------------
340    // ! Output
341    // ------------------------------------------------------------------------
342
343    protected function scroll(): void
344    {
345        $this->renderX = 0;
346        if ($this->cursor->y < $this->document->numRows)
347        {
348            $row = $this->document->row($this->cursor->y);
349
350            if ($row->isValid())
351            {
352                $this->renderX = $this->rowCxToRx($row, $this->cursor->x);
353            }
354
355        }
356
357        // Vertical Scrolling
358        if ($this->cursor->y < $this->offset->y)
359        {
360            $this->offset->y = $this->cursor->y;
361        }
362        else if ($this->cursor->y >= ($this->offset->y + $this->terminalSize->rows))
363        {
364            $this->offset->y = $this->cursor->y - $this->terminalSize->rows + 1;
365        }
366
367        // Horizontal Scrolling
368        if ($this->renderX < $this->offset->x)
369        {
370            $this->offset->x = $this->renderX;
371        }
372        else if ($this->renderX >= ($this->offset->x + $this->terminalSize->cols))
373        {
374            $this->offset->x = $this->renderX - $this->terminalSize->cols + 1;
375        }
376    }
377
378    protected function drawRows(): void
379    {
380        for ($y = 0; $y < $this->terminalSize->rows; $y++)
381        {
382            $fileRow = $y + $this->offset->y;
383
384            $this->outputBuffer .= ANSI::CLEAR_LINE;
385
386            ($fileRow >= $this->document->numRows)
387                ? $this->drawPlaceholderRow($y)
388                : $this->drawRow($fileRow);
389
390            $this->outputBuffer .= "\r\n";
391        }
392    }
393
394    protected function drawRow(int $rowIdx): void
395    {
396        $row = $this->document->row($rowIdx);
397        if ( ! $row->isValid())
398        {
399            return;
400        }
401
402        $len = $row->rsize - $this->offset->x;
403        if ($len < 0)
404        {
405            $len = 0;
406        }
407        if ($len > $this->terminalSize->cols)
408        {
409            $len = $this->terminalSize->cols;
410        }
411
412        $chars = substr($row->render, $this->offset->x, (int)$len);
413        $hl = array_slice($row->hl, $this->offset->x, (int)$len);
414
415        $currentColor = -1;
416
417        for ($i = 0; $i < $len; $i++)
418        {
419            $ch = $chars[$i];
420
421            // Handle 'non-printable' characters
422            if (is_ctrl($ch))
423            {
424                $sym = (ord($ch) <= 26)
425                    ? chr(ord('@') + ord($ch))
426                    : '?';
427                $this->outputBuffer .= ANSI::color(Color::INVERT);
428                $this->outputBuffer .= $sym;
429                $this->outputBuffer .= ANSI::RESET_TEXT;
430                if ($currentColor !== -1)
431                {
432                    $this->outputBuffer .= ANSI::color($currentColor);
433                }
434            }
435            else if ($hl[$i] === Highlight::NORMAL)
436            {
437                if ($currentColor !== -1)
438                {
439                    $this->outputBuffer .= ANSI::RESET_TEXT;
440                    $this->outputBuffer .= ANSI::color(Color::FG_WHITE);
441                    $currentColor = -1;
442                }
443                $this->outputBuffer .= $ch;
444            }
445            else
446            {
447                $color = syntax_to_color($hl[$i]);
448                if ($color !== $currentColor)
449                {
450                    $currentColor = $color;
451                    $this->outputBuffer .= ANSI::RESET_TEXT;
452                    $this->outputBuffer .= ANSI::color($color);
453                }
454                $this->outputBuffer .= $ch;
455            }
456        }
457
458        $this->outputBuffer .= ANSI::RESET_TEXT;
459        $this->outputBuffer .= ANSI::color(Color::FG_WHITE);
460    }
461
462    protected function drawPlaceholderRow(int $y): void
463    {
464        if ($this->document->numRows === 0 && $y === (int)($this->terminalSize->rows / 2))
465        {
466            $welcome = sprintf('PHP Kilo editor -- version %s', KILO_VERSION);
467            $welcomelen = strlen($welcome);
468            if ($welcomelen > $this->terminalSize->cols)
469            {
470                $welcomelen = $this->terminalSize->cols;
471            }
472
473            $padding = ($this->terminalSize->cols - $welcomelen) / 2;
474            if ($padding > 0)
475            {
476                $this->outputBuffer .= '~';
477                $padding--;
478            }
479            for ($i = 0; $i < $padding; $i++)
480            {
481                $this->outputBuffer .= ' ';
482            }
483
484            $this->outputBuffer .= substr($welcome, 0, $welcomelen);
485        }
486        else
487        {
488            $this->outputBuffer .= '~';
489        }
490    }
491
492    protected function drawStatusBar(): void
493    {
494        $this->outputBuffer .= ANSI::color(Color::INVERT);
495
496        $statusFilename = $this->document->filename !== '' ? $this->document->filename : '[No Name]';
497        $syntaxType = $this->document->fileType->name;
498        $isDirty = $this->document->isDirty() ? '(modified)' : '';
499        $status = sprintf('%.20s - %d lines %s', $statusFilename, $this->document->numRows, $isDirty);
500        $rstatus = sprintf('%s | %d/%d', $syntaxType, $this->cursor->y + 1, $this->document->numRows);
501        $len = strlen($status);
502        $rlen = strlen($rstatus);
503        if ($len > $this->terminalSize->cols)
504        {
505            $len = $this->terminalSize->cols;
506        }
507        $this->outputBuffer .= substr($status, 0, $len);
508        while ($len < $this->terminalSize->cols)
509        {
510            if ($this->terminalSize->cols - $len === $rlen)
511            {
512                $this->outputBuffer .= substr($rstatus, 0, $rlen);
513                break;
514            }
515
516            $this->outputBuffer .= ' ';
517            $len++;
518        }
519        $this->outputBuffer .= ANSI::RESET_TEXT;
520        $this->outputBuffer .= "\r\n";
521    }
522
523    protected function drawMessageBar(): void
524    {
525        $this->outputBuffer .= ANSI::CLEAR_LINE;
526        $len = $this->statusMessage->len;
527        if ($len > $this->terminalSize->cols)
528        {
529            $len = $this->terminalSize->cols;
530        }
531
532        // If there is a message, and it's been less than 5 seconds since
533        // last screen update, show the message
534        if ($len > 0 && (time() - $this->statusMessage->time) < 5)
535        {
536            $this->outputBuffer .= substr($this->statusMessage->text, 0, $len);
537        }
538    }
539
540    protected function refreshScreen(): void
541    {
542        $this->scroll();
543
544        $this->outputBuffer = ANSI::HIDE_CURSOR . ANSI::RESET_CURSOR;
545
546        $this->drawRows();
547        $this->drawStatusBar();
548        $this->drawMessageBar();
549
550        // Specify the current cursor position
551        $this->outputBuffer .= ANSI::moveCursor(
552            $this->cursor->y - $this->offset->y,
553            $this->renderX - $this->offset->x
554        );
555
556        $this->outputBuffer .= ANSI::SHOW_CURSOR;
557
558        Terminal::write($this->outputBuffer, strlen($this->outputBuffer));
559    }
560
561    // ------------------------------------------------------------------------
562    // ! Input
563    // ------------------------------------------------------------------------
564
565    protected function prompt(string $prompt, ?callable $callback = NULL): string
566    {
567        $buffer = '';
568        $modifiers = KeyType::getConstList();
569        while (TRUE)
570        {
571            $this->setStatusMessage($prompt, $buffer);
572            $this->refreshScreen();
573
574            $c = Terminal::readKey();
575            $isModifier = in_array($c, $modifiers, TRUE);
576
577            if ($c === KeyType::ESCAPE || ($c === RawKeyCode::ENTER && $buffer !== ''))
578            {
579                $this->setStatusMessage('');
580                if ($callback !== NULL)
581                {
582                    $callback($buffer, $c);
583                }
584                return ($c === RawKeyCode::ENTER) ? $buffer : '';
585            }
586
587            if ($c === KeyType::DELETE || $c === KeyType::BACKSPACE)
588            {
589                $buffer = substr($buffer, 0, -1);
590            }
591            else if (is_ascii($c) && ( ! (is_ctrl($c) || $isModifier)))
592            {
593                $buffer .= $c;
594            }
595
596            if ($callback !== NULL)
597            {
598                $callback($buffer, $c);
599            }
600        }
601    }
602
603    /**
604     * Input processing
605     */
606    protected function processKeypress(): void
607    {
608        $c = Terminal::readKey();
609
610        if ($c === RawKeyCode::NULL || $c === RawKeyCode::EMPTY)
611        {
612            return;
613        }
614
615        switch ($c)
616        {
617            case RawKeyCode::CTRL('q'):
618                $this->quitAttempt();
619                return;
620
621            case RawKeyCode::CTRL('s'):
622                $this->save();
623            break;
624
625            case RawKeyCode::CTRL('f'):
626                $this->find();
627            break;
628
629            case KeyType::DELETE:
630            case KeyType::BACKSPACE:
631                $this->removeChar($c);
632            break;
633
634            case KeyType::ARROW_UP:
635            case KeyType::ARROW_DOWN:
636            case KeyType::ARROW_LEFT:
637            case KeyType::ARROW_RIGHT:
638            case KeyType::PAGE_UP:
639            case KeyType::PAGE_DOWN:
640            case KeyType::HOME:
641            case KeyType::END:
642                $this->moveCursor($c);
643            break;
644
645            case RawKeyCode::CTRL('l'):
646            case KeyType::ESCAPE:
647                // Do nothing
648            break;
649
650            default:
651                $this->insertChar($c);
652            break;
653        }
654
655        // Reset quit confirmation timer on different keypress
656        if ($this->quitTimes < KILO_QUIT_TIMES)
657        {
658            $this->quitTimes = KILO_QUIT_TIMES;
659            $this->setStatusMessage('');
660        }
661    }
662
663    // ------------------------------------------------------------------------
664    // ! Editor operation helpers
665    // ------------------------------------------------------------------------
666
667    protected function moveCursor(string $key): void
668    {
669        $x = $this->cursor->x;
670        $y = $this->cursor->y;
671        $row = $this->document->row($y);
672        if ( ! $row->isValid())
673        {
674            return;
675        }
676
677        switch ($key)
678        {
679            case KeyType::ARROW_LEFT:
680                if ($x !== 0)
681                {
682                    $x--;
683                }
684                else if ($y > 0)
685                {
686                    // Beginning of a line, go to end of previous line
687                    $y--;
688                    $x = $row->size - 1;
689                }
690                break;
691
692            case KeyType::ARROW_RIGHT:
693                if ($x < $row->size)
694                {
695                    $x++;
696                }
697                else if ($x === $row->size)
698                {
699                    $y++;
700                    $x = 0;
701                }
702                break;
703
704            case KeyType::ARROW_UP:
705                if ($y !== 0)
706                {
707                    $y--;
708                }
709                break;
710
711            case KeyType::ARROW_DOWN:
712                if ($y < $this->document->numRows)
713                {
714                    $y++;
715                }
716                break;
717
718            case KeyType::PAGE_UP:
719                $y = saturating_sub($y, $this->terminalSize->rows);
720//                $y = ($y > $this->terminalSize->rows)
721//                    ? $y - $this->terminalSize->rows
722//                    : 0;
723                break;
724
725            case KeyType::PAGE_DOWN:
726                $y = saturating_add($y, $this->terminalSize->rows, $this->document->numRows);
727//                $y = ($y + $this->terminalSize->rows < $this->document->numRows)
728//                    ? $y + $this->terminalSize->rows
729//                    : $this->document->numRows;
730                break;
731
732            case KeyType::HOME:
733                $x = 0;
734                break;
735
736            case KeyType::END:
737                if ($y < $this->document->numRows)
738                {
739                    $x = $row->size;
740                }
741                break;
742
743            default:
744                // Do nothing
745        }
746
747        // Snap cursor to the end of a row when moving
748        // from a longer row to a shorter one
749        $row = $this->document->row($y);
750        if ($row->isValid())
751        {
752            if ($x > $row->size)
753            {
754                $x = $row->size;
755            }
756
757            $this->cursor->x = $x;
758            $this->cursor->y = $y;
759        }
760    }
761
762    protected function insertChar(string $c): void
763    {
764        $this->document->insert($this->cursor, $c);
765        $this->moveCursor(KeyType::ARROW_RIGHT);
766    }
767
768    protected function removeChar(string $ch): void
769    {
770        if ($ch === KeyType::DELETE)
771        {
772            $this->document->delete($this->cursor);
773        }
774
775        if ($ch === KeyType::BACKSPACE && ($this->cursor->x > 0 || $this->cursor->y > 0))
776        {
777            $this->moveCursor(KeyType::ARROW_LEFT);
778            $this->document->delete($this->cursor);
779        }
780    }
781
782    protected function quitAttempt(): void
783    {
784        if ($this->document->isDirty() && $this->quitTimes > 0)
785        {
786            if ($this->quitTimes === KILO_QUIT_TIMES)
787            {
788                Terminal::ding();
789            }
790
791            $this->setStatusMessage(
792                'WARNING!!! File has unsaved changes. Press Ctrl-Q %d more times to quit.',
793                $this->quitTimes
794            );
795
796            $this->quitTimes--;
797            return;
798        }
799
800        Terminal::clear();
801
802        $this->shouldQuit = true;
803    }
804}