вторник, 16 июня 2015 г.

Двуязычная проверка орфографии в Emacs

Я в последнее время почти полностью перешёл на Emacs, как универсальный редактор для всего, в связи с чем часто пишу там тексты. А поскольку уроки русского языка со школы уже основательно позабылись, то текст приходится часто копировать в LibreOffice для проверки орфографии, что надо признать жутко неудобно. Плюс ко всему я в текстах использую и английские слова, поэтому хотелось бы проверять сразу 2 языка. Найти готовую инструкцию по проверке орфографии в Emacs для такого случая не получилось. Поэтому пришлось разбираться со всем самостоятельно.




Итак, для начала выберем программу для проверки орфографии. Их довольно много, одной из самых хороших считается hunspell. Её используют OpenOffice, LibreOffice, Firefox и другие. К сожалению для выбранного мной плагина она работает немного не так, как нужно, поэтому я остановился на форке Hunspell - enchant, результаты, насколько я могу судить, она выдаёт такие же как и её родитель, отличия только в API. Словари для неё подходят те же, что и для Hunspell, тут я не специалист, выбрал те, что нашёл у себя в репозитории: русский и английский. Если постараться можно отыскать десятки других, в том числе например те, которые не считают использование "е" вместо "ё" ошибкой. Для archlinux поставить это можно вот так:
yaourt -S enchant hunspell-en hunspell-ru-aot
Что бы было понимание как все это работает, я покажу как использовать enchant из command line. Нам понадобятся 2 режима. В первом на вход будем подавать строку с текстом, который хотим проверить, а в ответ получим список слов, которые являются ошибочными:
$ echo "Привед, как у тибя дела? Helllo all right" | enchant -d ru_RU -l 
Привед
тибя
Helllo
all
right
Как ожидалось, русский словарь ничего про английский не знает, поэтому все английские слова пометил как ошибочные. Но мне то требуется двуязычная поддержка. Для решения этой проблемы предлагаются разные варианты, некоторые предлагают сливать 2 словаря в один, другие - вычленять из текста сначала русские слова и прогонять через русский словарь, потом английские - через английский. Оба варианта мне не нравятся, поэтому я просто написал скрипт, который результаты работы русского словаря прогоняет через английский, назвал его spell_check_text.sh, сделал его исполняемым и положил рядом с конфигом emacs:
#!/bin/bash
enchant -d ru_RU -l | enchant -d en_US -l
Протестируем работу:
$ echo "Привед, как у тибя дела? Helllo all right" | ./spell_check_text.sh
Привед
тибя
Helllo
Вуаля, теперь результаты правильные. Второй режим работы enchant о котором я говорил - позволяет получить варианты правильного написания слова по неправильному:
$ echo "Привед" | enchant -d ru_RU -a 
@(#) International Ispell Version 3.1.20 (but really Enchant 1.6.0)
& Привед 9 0: Приведи, Приведу, Приведя, Привод, Привад, Привес, Присед, Привет, Привей
Но поскольку нам опять же нужна проверка двух языков сразу я опять написал скрипт spell_check_word.sh и положил его рядом с spell_check_text.sh:
#!/bin/sh
read word;
echo $word | 
if [[ $word =~ [a-zA-Z] ]]
then
 enchant -d en_US -a
else
 enchant -d ru_RU -a
fi
Смысл думаю очевиден - если находим хоть один английский символ в слове - прогоняем его через английский словарь, иначе через русский.

Осталось прикрутить это все к emacs. Встроенные плагины показались мне очень медленными и мало настраиваемыми, хотя не исключаю, что я просто не умею их готовить. Поэтому я остановился на wcheck-mode, который позволяет настроить почти все что нужно, причём сделать это довольно просто. Плюс у него есть несколько оптимизаций по скорости работы - проверка выполняется в отдельном процессе, только для видимой части текста, только во время простоя, ну и ещё там несколько оптимизаций для увеличения скорости. Научим wcheck-mode правильно работать с enchant:
(defvar lcl-var:spelling-ignore nil)

(defun lcl:spelling-add-to-dictionary (marked-text)
  (let* ((word (downcase (aref marked-text 0)))
         (dict (if (string-match "[a-zA-Z]" word)
                   (message "en_US.dic")
                 (message "ru_RU.dic")))
         (file (concat "~/.config/enchant/" dict)))
    (when (and file (file-writable-p file))
      (with-temp-buffer
        (insert word) (newline)
        (append-to-file (point-min) (point-max) file)
        (message "Added word \"%s\" to the \"%s\" dictionary" word dict))
      (wcheck-mode 0)
      (wcheck-mode 1))))

(defun lcl:spelling-add-to-ignore (marked-text)
  (let ((word (aref marked-text 0)))
    (add-to-list 'lcl-var:spelling-ignore word)
    (message "Added word \"%s\" to the ignore list" word)
    (wcheck--hook-outline-view-change)))

(defun lcl:spelling-action-menu (marked-text)
  (append (wcheck-parser-ispell-suggestions)
          (list (cons "[Add to dictionary]" 'lcl:spelling-add-to-dictionary)
                (cons "[Ignore]" 'lcl:spelling-add-to-ignore))))

(defun lcl:delete-list (delete-list list)
  (dolist (el delete-list)
    (setq list (remove el list)))
  list)

(defun lcl:spelling-parser-lines (&rest ignored)
  (lcl:delete-list lcl-var:spelling-ignore
                   (delete-dups
                    (split-string
                     (buffer-substring-no-properties (point-min) (point-max))
                     "\n+" t))))

(defun cfg:spelling ()
  (require 'wcheck-mode)
  (defun wcheck--choose-action-minibuffer (actions)
    (cdr
     (assoc
      (ido-completing-read "Choose " (mapcar #'car actions))
      actions)))
  (setq-default
   wcheck-language "All"
   wcheck-language-data
   '(("All"
      (program . "~/.config/emacs/bin/spell_check_text.sh")
      (parser . lcl:spelling-parser-lines)
      (action-program . "~/.config/emacs/bin/spell_check_word.sh")
      (action-parser . lcl:spelling-action-menu)
      (read-or-skip-faces
       ((emacs-lisp-mode c-mode c++-mode python-mode)
        read font-lock-comment-face)
       (org-mode
        skip org-block-begin-line org-block-end-line org-meta-line org-link)
       (nil))
      ))))
(cfg:spelling)
Выглядит немного сложно, но я постараюсь объяснить что тут происходит. Вся настройка выполняется в функции "cfg:spelling", в ней, во-первых, переопределяется функция "wcheck–choose-action-minibuffer" из плагина, что бы список вариантов исправления выдавался через ido, можно конечно этого не делать и оставить оригинальную реализацию, но мне она не нравится. Дальше заполняется переменная "wcheck-language-data":
  • program
Какой script будем вызывать для нахождения всех ошибочных слов в тексте, тут я указал путь к описанному ранее spell_check_text.sh
  • parser
Это функция которая распарсит результаты работы скрипта из "program". Реализация lcl:spelling-parser-lines довольно очевидна, обращу лишь внимание на то, что я из результатов удаляю слова добавленные в "ignore" (про эту опция напишу ниже)
  • action-program
Тут указываем какой скрипт вызывать для того, что бы показать варианты правильного написания для слова для слова. Скрипт spell_check_word.sh - я тоже описывал ранее.
  • action-parser
Соответственно parser результатов вызова "action-program". Тут стоит обратить внимание, что помимо результатов я добавил вывод пунктов меню "Add to dictionary" и "Ignore". Если выбрать первый, то слово добавится в пользовательский русский или английский словарь enchant, которых находится тут: "~/.config/enchant/". Само добавление написано в "lcl:spelling-add-to-dictionary", принадлежность слова к определённому языку, опять же определяется по тому есть в нем английские символы или нет. А добавление слова в "Ignore" означает, что в текущей сессии работы с Emacs это слово не будет проверяться во всех буферах. Добавление происходит в "lcl:spelling-add-to-ignore" и фактически сводится к добавлению в переменную "lcl-var:spelling-ignore".
  • read-or-skip-faces
Интересная опция, в данной реализации она говорит, что в режимах "emacs-lisp-mode c-mode c++-mode python-mode" нужно проверять только тот текст, который имеет face "font-lock-comment-face", т.е. только в комментариях. А в режиме "org-mode" проверять весь текст кроме того, который имеет face один из: "org-block-begin-line org-block-end-line org-meta-line org-link", т.е. убираем проверку в служебных тегах и т.п. Если нужно, то вот функция которая поможет узнать текущий face под курсором:
(defun cfg:what-face (pos)
  (interactive "d")
  (let ((face (or (get-char-property (point) 'read-face-name)
                  (get-char-property (point) 'face))))
    (if face (message "Face: %s" face) (message "No face at %d" pos))))
Теперь осталось только по желанию навесить горячие клавиши на управление всем этим хозяйством:
wcheck-mode - функция включения\выключения режима проверки орфографии
wcheck-actions - показать варианты исправления для слова под курсором
wcheck-jump-forward - перейти к следующей ошибке
wcheck-jump-backward - перейти к предыдущей ошибке
В работе выглядит проверка вот так:

Получилось слегка затянуто, но я постарался подробно описать что и откуда берётся, что бы можно было при необходимости доработать под себя.

3 комментария:

  1. Большое спасибо за рецепт. После определённых плясок всё заработало.

    У меня Fedora 22, поэтому словарь пришлось вручную скачать по ссылке. oxt - это на самом деле zip, поэтому разархивируем его и копируем .dic и .aff в /usr/share/myspell/. Чтобы не заменять установленный словарь, я установил новый под именем russian-aot. Соответственно, в обоих скриптах надо поменять название словаря. После этого надо дать read permissions group и others: chmod g+r , chmod o+r .

    Ещё пришлось создать директорию, которая упоминается в (file (concat "~/.config/enchant/" dict))). В моём случае, я изменил путь на ~/.enchant и создал эту директорию.

    ОтветитьУдалить
  2. Ох, рано обрадовался. Почему-то добавление в словарь не работает - слово всё равно подсвечивается, как неправильное.

    ОтветитьУдалить
    Ответы
    1. А не работает - не добавляет слово в пользовательский словарь в директорию "~/.config/enchant/" (ну в твоем случае в "~/.enchant") или оно добавлено, но не подхватывается enchant-ом?

      Удалить