Skip to content

Feature: Support to navigate problems in LC-Problems buffer #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@ LeetCode brings you offer, and now Emacs brings you LeetCode!
| n | cursor move down |
| p | cursor move up |
| l | change prefer language |
| o | show current problem |
| O | show problem by id |
| v | view current problem |
| V | view problem by id |
| b | show current problem in browser |
| B | show problem by id in browser |
| c | solve current problem |
| C | solve problem by id |
| s | filter problems by regex |
| t | filter problems by tag |
| d | filter problems by difficulty |
| / | clear filters |
| g | refresh without fetching from LeetCode |
| G | refresh all data |
| RET | show current problem description |
| TAB | view current problem description |

2. Press `<RET>`, show problem description, move cursor to "solve it", press
`<RET>` again, start coding!
Expand Down
257 changes: 177 additions & 80 deletions leetcode.el
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,28 @@ row."
(lambda (col size) (list col size nil))
header-names widths))))

(defun leetcode--stringify-difficulty (difficulty)
"Stringify DIFFICULTY level (number) to 'easy', 'medium' or 'hard'."
(let ((easy-tag "easy")
(medium-tag "medium")
(hard-tag "hard"))
(cond
((eq 1 difficulty)
(prog1 easy-tag
(put-text-property
0 (length easy-tag)
'font-lock-face 'leetcode-easy-face easy-tag)))
((eq 2 difficulty)
(prog1 medium-tag
(put-text-property
0 (length medium-tag)
'font-lock-face 'leetcode-medium-face medium-tag)))
((eq 3 difficulty)
(prog1 hard-tag
(put-text-property
0 (length hard-tag)
'font-lock-face 'leetcode-hard-face hard-tag))))))

(defun leetcode--problems-rows ()
"Generate tabulated list rows from `leetcode--all-problems'.
Return a list of rows, each row is a vector:
Expand Down Expand Up @@ -443,22 +465,7 @@ Return a list of rows, each row is a vector:
;; acceptance
(plist-get p :acceptance)
;; difficulty
(cond
((eq 1 (plist-get p :difficulty))
(prog1 easy-tag
(put-text-property
0 (length easy-tag)
'font-lock-face 'leetcode-easy-face easy-tag)))
((eq 2 (plist-get p :difficulty))
(prog1 medium-tag
(put-text-property
0 (length medium-tag)
'font-lock-face 'leetcode-medium-face medium-tag)))
((eq 3 (plist-get p :difficulty))
(prog1 hard-tag
(put-text-property
0 (length hard-tag)
'font-lock-face 'leetcode-hard-face hard-tag))))
(leetcode--stringify-difficulty (plist-get p :difficulty))
;; tags
(string-join (plist-get p :tags) ", "))
rows)))
Expand Down Expand Up @@ -676,14 +683,14 @@ LeetCode require slug-title as the request parameters."
(cond
((eq .status_code 10)
(insert "Output:\n")
(dotimes (i (length .code_answer))
(insert (aref .code_answer i))
(insert "\n"))
(dotimes (i (length .code_answer))
(insert (aref .code_answer i))
(insert "\n"))
(insert "\n")
(insert "Expected:\n")
(dotimes (i (length .expected_code_answer))
(insert (aref .expected_code_answer i))
(insert "\n"))
(dotimes (i (length .expected_code_answer))
(insert (aref .expected_code_answer i))
(insert "\n"))
(insert "\n"))
((eq .status_code 14)
(insert .status_msg))
Expand Down Expand Up @@ -877,24 +884,22 @@ following possible value:
"Generate problem link from TITLE."
(concat leetcode--base-url "/problems/" (leetcode--slugify-title title)))

(aio-defun leetcode-show-current-problem ()
"Show current entry problem description.
Get current entry by using `tabulated-list-get-entry' and use
`shr-render-buffer' to render problem description."
(interactive)
(let* ((entry (tabulated-list-get-entry))
(pos (aref entry 1))
(title (substring-no-properties (aref entry 2) nil -2)) ;strip paid mark
(difficulty (aref entry 4))
(problem (aio-await (leetcode--fetch-problem title)))
(defun leetcode--show-problem (problem problem-info)
"Show the description of PROBLEM, whose meta data is PROBLEM-INFO.
Use `shr-render-buffer' to render problem description. This action
will show the description in other window and jump to it."
(let* ((problem-id (plist-get problem-info :id))
(title (plist-get problem-info :title))
(difficulty-level (plist-get problem-info :difficulty))
(difficulty (leetcode--stringify-difficulty difficulty-level))
(buf-name leetcode--description-buffer-name)
(html-margin "&nbsp;&nbsp;&nbsp;&nbsp;"))
(leetcode--debug "select title: %s" title)
(let-alist problem
(when (get-buffer buf-name)
(kill-buffer buf-name))
(with-temp-buffer
(insert (concat "<h1>" pos ". " title "</h1>"))
(insert (concat "<h1>" (number-to-string problem-id) ". " title "</h1>"))
(insert (concat (capitalize difficulty) html-margin
"likes: " (number-to-string .likes) html-margin
"dislikes: " (number-to-string .dislikes)))
Expand All @@ -910,10 +915,7 @@ Get current entry by using `tabulated-list-get-entry' and use
(insert (make-string 4 ?\s))
(insert-text-button "solve it"
'action (lambda (btn)
(leetcode--start-coding
title
(append .codeSnippets nil)
.sampleTestCase))
(leetcode--start-coding problem problem-info))
'help-echo "solve the problem.")
(insert (make-string 4 ?\s))
(insert-text-button "link"
Expand All @@ -924,6 +926,77 @@ Get current entry by using `tabulated-list-get-entry' and use
(leetcode--problem-description-mode)
(switch-to-buffer (current-buffer))))))

(aio-defun leetcode-show-problem (problem-id)
"Show the description of problem with id PROBLEM-ID.
Get problem by id and use `shr-render-buffer' to render problem
description. This action will show the description in other
window and jump to it."
(interactive (list (read-number "Show problem by problem id: "
(leetcode--get-current-problem-id))))
(let* ((problem-info (leetcode--get-problem-by-id problem-id))
(title (plist-get problem-info :title))
(problem (aio-await (leetcode--fetch-problem title))))
(leetcode--show-problem problem problem-info)))

(defun leetcode-show-current-problem ()
"Show current problem's description.
Call `leetcode-show-problem' on the current problem id. This
action will show the description in other window and jump to it."
(interactive)
(leetcode-show-problem (leetcode--get-current-problem-id)))

(aio-defun leetcode-view-problem (problem-id)
"View problem by PROBLEM-ID while staying in `LC Problems' window.
Similar with `leetcode-show-problem', but instead of jumping to the
description window, this action will jump back in `LC Problems'."
(interactive (list (read-number "View problem by problem id: "
(leetcode--get-current-problem-id))))
(aio-await (leetcode-show-problem problem-id))
(leetcode--jump-to-window-by-buffer-name leetcode--buffer-name))

(defun leetcode-view-current-problem ()
"View current problem while staying in `LC Problems' window.
Similar with `leetcode-show-current-problem', but instead of jumping to
the description window, this action will jump back in `LC Problems'."
(interactive)
(leetcode-view-problem (leetcode--get-current-problem-id)))

(defun leetcode-show-problem-in-browser (problem-id)
"Open the problem with id PROBLEM-ID in browser."
(interactive (list (read-number "Show in browser by problem id: "
(leetcode--get-current-problem-id))))
(let* ((problem (leetcode--get-problem-by-id problem-id))
(title (plist-get problem :title))
(link (leetcode--problem-link title)))
(leetcode--debug "Open in browser: %s" link)
(browse-url link)))

(defun leetcode-show-current-problem-in-browser ()
"Open the current problem in browser.
Call `leetcode-show-problem-in-browser' on the current problem id."
(interactive)
(leetcode-show-problem-in-browser (leetcode--get-current-problem-id)))

(aio-defun leetcode-solve-problem (problem-id)
"Start coding the problem with id PROBLEM-ID."
(interactive (list (read-number "Solve the problem with id: "
(leetcode--get-current-problem-id))))
(let* ((problem-info (leetcode--get-problem-by-id problem-id))
(title (plist-get problem-info :title))
(problem (aio-await (leetcode--fetch-problem title))))
(leetcode--show-problem problem problem-info)
(leetcode--start-coding problem problem-info)))

(defun leetcode-solve-current-problem ()
"Start coding the current problem.
Call `leetcode-solve-problem' on the current problem id."
(interactive)
(leetcode-solve-problem (leetcode--get-current-problem-id)))

(defun leetcode--jump-to-window-by-buffer-name (buffer-name)
"Jump to window by BUFFER-NAME."
(select-window (get-buffer-window buffer-name)))

(defun leetcode--kill-buff-and-delete-window (buf)
"Kill BUF and delete its window."
(delete-windows-on buf t)
Expand Down Expand Up @@ -1010,66 +1083,90 @@ python3, ruby, rust, scala, swift, mysql, mssql, oraclesql.")
(plist-get p :title))))
(plist-get leetcode--all-problems :problems)))

(defun leetcode--get-problem-by-id (id)
"Get problem from `leetcode--all-problems' by ID."
(let ((p (seq-find (lambda (p) (equal id (plist-get p :id)))
(plist-get leetcode--all-problems :problems))))
(if p p (user-error "Not found: No such problem with given id `%s'" id))))

(defun leetcode--get-problem-id (slug-title)
"Get problem id by SLUG-TITLE."
(let ((problem (leetcode--get-problem slug-title)))
(plist-get problem :id)))

(defun leetcode--start-coding (title snippets testcase)
(defun leetcode--get-current-problem-id ()
"Get id of the current problem."
(string-to-number (aref (tabulated-list-get-entry) 1)))

(defun leetcode--start-coding (problem problem-info)
"Create a buffer for coding.
The buffer will be not associated with any file. It will choose
major mode by `leetcode-prefer-language'and `auto-mode-alist'.
TITLE is a problem title. SNIPPETS is a list of alist used to
store eachprogramming language's snippet. TESTCASE is provided
for current problem."
(add-to-list 'leetcode--problem-titles title)
(leetcode--solving-layout)
(leetcode--set-lang snippets)
(let* ((slug-title (leetcode--slugify-title title))
(problem-id (leetcode--get-problem-id slug-title))
(buf-name (leetcode--get-code-buffer-name title))
(code-buf (get-buffer buf-name))
(suffix (assoc-default
leetcode--lang
leetcode--lang-suffixes)))
(unless code-buf
(with-current-buffer (leetcode--get-code-buffer buf-name)
(setq code-buf (current-buffer))
(funcall (assoc-default suffix auto-mode-alist #'string-match-p))
(let* ((snippet (seq-find (lambda (s)
(equal (alist-get 'langSlug s)
leetcode--lang))
snippets))
(template-code (alist-get 'code snippet)))
(unless (save-mark-and-excursion
(goto-char (point-min))
(search-forward (string-trim template-code) nil t))
(insert template-code))
(leetcode--replace-in-buffer "" ""))))

(display-buffer code-buf
'((display-buffer-reuse-window
leetcode--display-code)
(reusable-frames . visible))))
(with-current-buffer (get-buffer-create leetcode--testcase-buffer-name)
(erase-buffer)
(insert testcase)
(display-buffer (current-buffer)
'((display-buffer-reuse-window
leetcode--display-testcase)
(reusable-frames . visible))))
(with-current-buffer (get-buffer-create leetcode--result-buffer-name)
(erase-buffer)
(display-buffer (current-buffer)
'((display-buffer-reuse-window
leetcode--display-result)
(reusable-frames . visible)))))
(let-alist problem
(let* ((title (plist-get problem-info :title))
(snippets (append .codeSnippets nil))
(testcase .sampleTestCase))
(add-to-list 'leetcode--problem-titles title)
(leetcode--solving-layout)
(leetcode--set-lang snippets)
(let* ((slug-title (leetcode--slugify-title title))
(problem-id (leetcode--get-problem-id slug-title))
(buf-name (leetcode--get-code-buffer-name title))
(code-buf (get-buffer buf-name))
(suffix (assoc-default
leetcode--lang
leetcode--lang-suffixes)))
(unless code-buf
(with-current-buffer (leetcode--get-code-buffer buf-name)
(setq code-buf (current-buffer))
(funcall (assoc-default suffix auto-mode-alist #'string-match-p))
(let* ((snippet (seq-find (lambda (s)
(equal (alist-get 'langSlug s)
leetcode--lang))
snippets))
(template-code (alist-get 'code snippet)))
(unless (save-mark-and-excursion
(goto-char (point-min))
(search-forward (string-trim template-code) nil t))
(insert template-code))
(leetcode--replace-in-buffer "" ""))))

(display-buffer code-buf
'((display-buffer-reuse-window
leetcode--display-code)
(reusable-frames . visible))))
(with-current-buffer (get-buffer-create leetcode--testcase-buffer-name)
(erase-buffer)
(insert testcase)
(display-buffer (current-buffer)
'((display-buffer-reuse-window
leetcode--display-testcase)
(reusable-frames . visible))))
(with-current-buffer (get-buffer-create leetcode--result-buffer-name)
(erase-buffer)
(display-buffer (current-buffer)
'((display-buffer-reuse-window
leetcode--display-result)
(reusable-frames . visible))))
)))

(defvar leetcode--problems-mode-map
(let ((map (make-sparse-keymap)))
(prog1 map
(suppress-keymap map)
(define-key map (kbd "RET") #'leetcode-show-current-problem)
(define-key map (kbd "TAB") #'leetcode-view-current-problem)
(define-key map "o" #'leetcode-show-current-problem)
(define-key map "O" #'leetcode-show-problem)
(define-key map "v" #'leetcode-view-current-problem)
(define-key map "V" #'leetcode-view-problem)
(define-key map "b" #'leetcode-show-current-problem-in-browser)
(define-key map "B" #'leetcode-show-problem-in-browser)
(define-key map "c" #'leetcode-solve-current-problem)
(define-key map "C" #'leetcode-solve-problem)
(define-key map "n" #'next-line)
(define-key map "p" #'previous-line)
(define-key map "s" #'leetcode-set-filter-regex)
Expand Down