reddit / emacs

Lookup word under cursor, browse synonyms, and insert selection (works with Embark actions)

WallyMetropolis · 2022-07-27 18:04
Somewhat inspired by Hyberbole's `hui-search-web` Dictionary lookup, I wanted an interactive dictionary and thesaurus lookup capability that I could integrate with Embark to get definitions and synonyms for the word under the cursor. It turns out that [dictionary.com](https://dictionary.com) has an API that's free to use for personal, non-commercial use under 1,000 requests per day. These functions are built with `completing-read` so they play very nicely with ido, vertico, selectrum, consult etc. I haven't tried them with Helm or Ivy. Because they're interactive functions, it's trivial to add these as Embark actions. Specifically, I added them to the `embark-identifier-map` and `embark-region-map`. The later allows me to select a word (with something like `expand-region`, or any other way to mark that word as the active region) call embark-act, browse through synonyms, and replace the selected word with the chosen synonym. (define-key embark-identifier-map "DD" #'metropolis/get-definition) (define-key embark-identifier-map "DT" #'metropolis/get-synonyms) (define-key embark-region-map "DD" #'metropolis/get-definition) (define-key embark-region-map "DT" #'metropolis/get-synonyms) The attached gif shows this workflow with Embark, Vertico, and Consult. All that's required to configure is to create an account on [dictionary.com](https://dictionary.com), generate API keys, and supply those with the environment variables `DICTIONARY_API_KEY` and `THESAURUS_API_KEY`. Here's the code to do this. It depends on dash.el. If there's interest, I can publish this in some capacity or another. (defun metropolis/thesaurus-response-buffer-to-formatted-string (rbuffer) """Take the results of json-parse-buffer, RBUFFER, that was applied to the thesaurus api response, and format the parts we want into a nice string for browsing """ ;; The thesaurus API gives a specific match of a particular word per item, and each item's id distinguishes ;; it from other possible meanings or uses of that word. syns is a list of synonyms associated to that id. ;; Flatten the results to remove the nested lists that sometimes are retunred. ;; Then format it as id: (list of synonyms) (-map (lambda (n) (format "%s: %s" (alist-get 'id (alist-get 'meta n)) (flatten-tree (alist-get 'syns (alist-get 'meta n))))) rbuffer)) (defun metropolis/request-thesaurus (word api-key) (with-temp-buffer (url-insert-file-contents (format "https://www.dictionaryapi.com/api/v3/references/thesaurus/json/%s?key=%s" word api-key)) (metropolis/thesaurus-response-buffer-to-formatted-string (json-parse-buffer :object-type 'alist :array-type 'list)))) (defun metropolis/defs-and-sysnonyms-to-clean-syns (selected-defs-and-synonyms) ;; The regex filters out all non-alphanumeric characters, but leaves accented Latin characters. ;; We take the cdr or the split-string so we only complete against the synonyms, not including the id. (cdr (split-string (replace-regexp-in-string "[^a-zA-Z\d\s\u00C0-\u00FF]" "" selected-defs-and-synonyms)))) (defun metropolis/get-synonyms (word) "Send a request to dictionary.com to fetch synonyms of WORD. Insert the selected word into the buffer." (interactive "sWord: ") (let* ((api-key (getenv "THESAURUS_API_KEY")) (defs-and-synonyms (metropolis/request-thesaurus word api-key)) (selected-defs-and-synonyms (completing-read "Select synonyms: " defs-and-synonyms)) (selected-synonym (completing-read "Synonyms: " (metropolis/defs-and-sysnonyms-to-clean-syns selected-defs-and-synonyms)))) (progn (if mark-active (delete-region (region-beginning) (region-end)) nil) (insert selected-synonym)))) (defun metropolis/get-definition (word) "Send a request to dictionary.com to fetch the definition of WORD" (interactive "sWord: ") (let ((api-key (getenv "DICTIONARY_API_KEY"))) (completing-read "Definitions: " (with-temp-buffer (url-insert-file-contents (format "https://www.dictionaryapi.com/api/v3/references/collegiate/json/%s?key=%s" word api-key)) (-map (apply-partially 'alist-get 'shortdef) (json-parse-buffer :object-type 'alist :array-type 'list)))))) `get-definition` and `get-synonyms` are the only functions to care about. The rest are internal. Any and all feedback welcome! https://i.redd.it/r1clpt8zh5e91.gif
ieure · 2022-07-27 18:30
I've been using [wordnut](https://github.com/gromnitsky/wordnut) for this, which uses Wordnet as its backend.
WallyMetropolis · 2022-07-27 19:02
Looks interesting. Couple questions about it: is it inconvenient to use this as the major mode? Seems like it should perhaps be a minor mode. Second, apart from looking up definitions, can you look up synonyms and insert them into the buffer? (I see now that it's basically *entirely* synonyms. Very cool project!) I'll definitely check out Wordnet as an alternative backend. Thanks for pointing this out.
ieure · 2022-07-27 19:41
> it inconvenient to use this as the major mode? Seems like it should perhaps be a minor mode. You call `wordnut-lookup-current-word` or `wordnut-search` to do the lookup (bound to whatever makes sense for you). The major mode is for showing the results. > part from looking up definitions, can you look up synonyms and insert them into the buffer? I don't think so, but seems like you could add that.
WallyMetropolis · 2022-07-27 20:01
Ah, gotcha. That makes sense. Thanks.
mickeyp · 2022-07-27 20:52
You might want to look at the builtin dictionary support in Emacs 28. Try `dictionary-lookup-definition` and friends.
Phil-Hudson · 2022-07-29 11:16
Since you're using `dash.el`, you can replace: (lambda (n) (format "%s: %s" (alist-get 'id (alist-get 'meta n)) (flatten-tree (alist-get 'syns (alist-get 'meta n))))) with: (-lambda ((&alist 'meta)) (-let (((&alist 'id 'syns) meta)) (format "%s: %s" id (flatten-tree syns)))
Phil-Hudson · 2022-07-29 11:19
Meta-feedback: it's a good idea to set your `fill-column` to 72 and re-fill (which is what Emacs calls line-wrapping) before posting code online.
Phil-Hudson · 2022-07-29 11:24
Somehow you've allowed a Python doc comment into an elisp function. It might actually compile and run, come to think of it, but only by accident.
WallyMetropolis · 2022-07-29 14:18
Cool tip. I started off without dash but it was getting ugly. I can definitely clean some of this up a bit more, yeah.
WallyMetropolis · 2022-07-29 14:18
Why 72?
Phil-Hudson · 2022-07-29 14:21
a) Ancient tradition b) Likely to work on even tablet screens
Phil-Hudson · 2022-08-02 11:35
Or again: (lambda (n) (let-alist n (format "%s: %s" .meta.id (flatten-tree .meta.syns))))
WallyMetropolis · 2022-08-02 15:04
Appreciate the code review. Sincerely. Very helpful for me.
[deleted] · 2022-08-02 23:33
[removed]
atanasj · 2022-08-02 23:41
This looks great! Do you need a developer account to get the API, because I cannot seem to find how to generate one…
WallyMetropolis · 2022-08-03 06:25
Yes, but it's free to set up and takes just a few minutes.
atanasj · 2022-08-03 06:57
Okay… How do you set it up? I coudn't find out how to sign up for a developer account… Could you please post a link?
WallyMetropolis · 2022-08-03 16:12
Register: [https://dictionaryapi.com/register/index](https://dictionaryapi.com/register/index) API Keys: https://dictionaryapi.com/account/my-keys