Краткое описание: Продолжаю историю CraftHub — кроссплатформенного редактора, который показывает JSON как таблицу. В прошлых частях мы разбирались с табличным редактированием и расширением для VS Code. В этот раз — самое частое пожелание из issue: «дайте работать с директорией, а не открывать файлы через диалог каждый раз». Рассказываю, как я прикрутил файловый проводник в стиле VS Code, почему сохранение в файл оказалось не таким тривиальным, и на какие грабли Avalonia наступил по дороге.

Для тех, кто пропустил предыдущие серии

CraftHub — это open-source редактор JSON, который открывает массив объектов не как «простыню» текста, а как таблицу: колонки — это поля, строки — элементы массива. Есть десктопная версия на Avalonia (Windows/macOS/Linux) и расширение для VS Code. Идея простая: перестать редактировать данные вручную в текстовом виде и ошибаться в запятых.

Проект живёт за счёт обратной связи. И самое частое, что просили в последнее время, звучало примерно так:

«Неудобно каждый раз открывать файл через диалог. Хочу видеть папку проекта и переключаться между файлами, как в VS Code».

Это стало issue про работу в директории. В этой статье — как я его закрывал.

Проблема: диалог открытия файла — это трение

Раньше цикл работы выглядел так: «Импорт JSON» → системный диалог → найти файл → открыть → поправить → «Экспорт» → снова диалог → выбрать путь. Если файлов в проекте десяток и ты прыгаешь между ними, это утомляет. Каждое переключение — несколько кликов и потеря контекста.

По сути не хватало двух вещей:

  1. Навигации — видеть дерево файлов проекта и открывать их в один-два клика.

  2. Нормального сохранения — чтобы Ctrl+S писал в тот же файл, а не спрашивал путь заново.

Звучит как «просто добавь панель слева». На практике вторая часть оказалась интереснее первой.

Решение: панель проводника в стиле VS Code

Первое — сама панель. Выбираешь корневую папку проекта, и слева появляется дерево: папки и .json-файлы. Двойной клик — файл открывается во вкладке. Есть фильтр (по умолчанию показываем только JSON, чтобы не тонуть в мусоре), поиск по файлам и кнопка показать/скрыть панель. Состояние — корневая папка, ширина, видимость — запоминается между запусками.

Ленивое дерево, чтобы не подвешивать UI

Наивная реализация — рекурсивно обойти всю папку при открытии — умрёт на большом проекте (привет, node_modules). Поэтому дерево грузится лениво, как в нормальных проводниках: у папки, в которой есть содержимое, добавляется «пустышка»-заглушка, чтобы отрисовать стрелку раскрытия, а реальные дети подгружаются только в момент разворачивания.

partial void OnIsExpandedChanged(bool value)
{
    if (value && IsDirectory && !_childrenLoaded)
        LoadChildren(); // читаем диск только когда узел реально раскрыли
}

Самое интересное: сохранение обратно в файл

Вот тут пришлось подумать про модель. У вкладки раньше не было понятия «я привязана к файлу на диске». Импорт просто читал содержимое, экспорт каждый раз спрашивал путь. Чтобы Ctrl+S работал как в редакторе, я ввёл у вкладки состояние «связана с файлом»:

  • Открыл файл (из проводника или через «Импорт») → вкладка запоминает путь.

  • Ctrl+S сериализует данные и пишет в этот же файл, без диалога.

  • Если файла ещё нет (новая вкладка) → Ctrl+S предлагает «Сохранить как», по умолчанию в текущую папку проекта, и после этого вкладка привязывается к новому файлу.

Дальше пошли нюансы, без которых «сохранение» ощущается сломанным:

Индикатор несохранённого. На вкладке загорается точка ●, если данные изменены. Сначала я хотел ловить это через события коллекций строк — но они срабатывают и при импорте, и пришлось бы городить флаги «сейчас идёт загрузка». В итоге сигналом «данные поменялись» стал сам сервис Undo/Redo: раз действие попало в историю — значит, пользователь что-то изменил.

Внешние изменения. Если файл на диске поменяли другой программой (сравниваю время модификации), перед перезаписью честно спрашиваю, а не молча затираю чужие правки.

Переименование = переименование файла. Логичное следствие модели: переименовал вкладку — переименовался и файл на диске (с сохранением расширения), а дерево обновилось. Иначе получается враньё: подпись одна, файл другой.

Файловые операции: как в проводнике

Раз уж есть дерево — от него ждут поведения файлового менеджера. Добавил прямо в панель:

  • создание нового файла (в корне или в конкретной папке);

  • удаление — через контекстное меню и по клавише Delete;

  • копирование / вырезание / вставку файлов (Ctrl+C / Ctrl+X / Ctrl+V);

  • drag & drop — перетащить файл или папку в другую папку;

  • «показать в системном проводнике» и «скопировать путь».

Копирование-вставка и drag & drop под капотом ходят в один и тот же движок переноса: он разруливает конфликты имён (создаёт name copy.json), не даёт бросить папку саму в себя или в свою подпапку, и обновляет только те узлы дерева, которых коснулись, а не перестраивает всё целиком.

Что дальше

Дорожную карту во многом рисует не мой список, а ваши issue. Если чего-то не хватает или что-то раздражает — заходите в репозиторий и напишите. Ровно так появилась и эта фича.

  • GitHub: https://github.com/c3n9/CraftHub

  • Прошлые части — про табличное редактирование и расширение для VS Code — в моём профиле.

Спасибо, что читаете и подсказываете.