aboutsummaryrefslogtreecommitdiffstats
path: root/packages/packages-leetcode.el
diff options
context:
space:
mode:
Diffstat (limited to 'packages/packages-leetcode.el')
-rw-r--r--packages/packages-leetcode.el1386
1 files changed, 1386 insertions, 0 deletions
diff --git a/packages/packages-leetcode.el b/packages/packages-leetcode.el
new file mode 100644
index 0000000..a192b80
--- /dev/null
+++ b/packages/packages-leetcode.el
@@ -0,0 +1,1386 @@
+;;; leetcode.el --- An leetcode client -*- lexical-binding: t; no-byte-compile: t -*-
+
+;; Copyright (C) 2019 Wang Kai
+
+;; Author: Wang Kai <kaiwkx@gmail.com>
+;; Keywords: extensions, tools
+;; Package-Version: 20220206.1515
+;; Package-Commit: b3103bd08c8943091f702c66d674f0f27ef7fe0b
+;; URL: https://github.com/kaiwk/leetcode.el
+;; Package-Requires: ((emacs "26") (dash "2.16.0") (graphql "0.1.1") (spinner "1.7.3") (aio "1.0") (log4e "0.3.3"))
+;; Version: 0.1.24
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; leetcode.el is an unofficial LeetCode client.
+;;
+;; Now it implements several API:
+;; - Check problems list
+;; - Try testcase
+;; - Submit code
+;;
+;; Since most HTTP requests works asynchronously, it won't block Emacs.
+;;
+;;; Code:
+(eval-when-compile
+ (require 'let-alist))
+
+(require 'json)
+(require 'shr)
+(require 'seq)
+(require 'subr-x)
+(require 'mm-url)
+(require 'cl-lib)
+
+(require 'dash)
+(require 'graphql) ; Some requests of LeetCode use GraphQL
+(require 'aio)
+(require 'spinner)
+(require 'log4e)
+
+(log4e:deflogger "leetcode" "%t [%l] %m" "%H:%M:%S" '((fatal . "fatal")
+ (error . "error")
+ (warn . "warn")
+ (info . "info")
+ (debug . "debug")
+ (trace . "trace")))
+(setq log4e--log-buffer-leetcode "*leetcode-log*")
+
+;;;###autoload
+(defun leetcode-toggle-debug ()
+ "Toggle debug."
+ (interactive)
+ (if (leetcode--log-debugging-p)
+ (progn
+ (leetcode--log-set-level 'info)
+ (leetcode--log-disable-debugging)
+ (message "leetcode disable debug"))
+ (progn
+ (leetcode--log-set-level 'debug)
+ (leetcode--log-enable-debugging)
+ (message "leetcode enable debug"))))
+
+(defun leetcode--install-my-cookie ()
+ "Install leetcode dependencies."
+ (let ((async-shell-command-display-buffer t))
+ (async-shell-command
+ "pip3 install my_cookies"
+ (get-buffer-create "*leetcode-install*"))))
+
+(defun leetcode--check-deps ()
+ "Check if all dependencies installed."
+ (if (executable-find "my_cookies")
+ t
+ (leetcode--install-my-cookie)
+ nil))
+
+(defgroup leetcode nil
+ "A Leetcode client."
+ :prefix 'leetcode-
+ :group 'tools)
+
+(defvar leetcode--user nil
+ "User object.
+The object with following attributes:
+:username String
+:solved Number
+:easy Number
+:medium Number
+:hard Number")
+
+(defvar leetcode--all-problems nil
+ "Problems info with a list of problem object.
+The object with following attributes:
+:num Number
+:tag String
+:problems List
+
+The elements of :problems has attributes:
+:status String
+:id Number
+:backend-id Number
+:title String
+:acceptance String
+:difficulty Number {1,2,3}
+:paid-only Boolean {t|nil}
+:tags List")
+
+(defvar leetcode--all-tags nil
+ "All problems tags.")
+
+(defvar leetcode--problem-titles nil
+ "Problem titles that have been open in solving layout.")
+
+(defvar leetcode-retry-threshold 20 "`leetcode-try' or `leetcode-submit' retry times.")
+(defvar leetcode--filter-regex nil "Filter rows by regex.")
+(defvar leetcode--filter-tag nil "Filter rows by tag.")
+(defvar leetcode--filter-difficulty nil
+ "Filter rows by difficulty, it can be \"easy\", \"medium\" and \"hard\".")
+(defconst leetcode--all-difficulties '("easy" "medium" "hard"))
+
+(defconst leetcode--paid "•" "Paid mark.")
+(defconst leetcode--checkmark "✓" "Checkmark for accepted problem.")
+(defconst leetcode--buffer-name "*leetcode*")
+(defconst leetcode--description-buffer-name "*leetcode-description*")
+(defconst leetcode--testcase-buffer-name "*leetcode-testcase*")
+(defconst leetcode--result-buffer-name "*leetcode-result*")
+
+(defface leetcode-paid-face
+ '((t (:foreground "gold")))
+ "Face for `leetcode--paid'."
+ :group 'leetcode)
+
+(defface leetcode-checkmark-face
+ '((t (:foreground "#5CB85C")))
+ "Face for `leetcode--checkmark'."
+ :group 'leetcode)
+
+(defface leetcode-easy-face
+ '((t (:foreground "#5CB85C")))
+ "Face for easy problems."
+ :group 'leetcode)
+
+(defface leetcode-medium-face
+ '((t (:foreground "#F0AD4E")))
+ "Face for medium problems."
+ :group 'leetcode)
+
+(defface leetcode-hard-face
+ '((t (:foreground "#D9534E")))
+ "Face for hard problems."
+ :group 'leetcode)
+
+;;; Login
+;; URL
+(defconst leetcode--domain "leetcode.cn")
+(defconst leetcode--base-url "https://leetcode.cn")
+(defconst leetcode--url-login (concat leetcode--base-url "/accounts/login"))
+
+;; Header
+(defconst leetcode--User-Agent '("User-Agent" .
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:66.0) Gecko/20100101 Firefox/66.0"))
+(defconst leetcode--X-Requested-With '("X-Requested-With" . "XMLHttpRequest"))
+(defconst leetcode--X-CSRFToken "X-CSRFToken")
+
+;; API
+(defconst leetcode--api-root (concat leetcode--base-url "/api"))
+(defconst leetcode--api-graphql (concat leetcode--base-url "/graphql"))
+(defconst leetcode--api-all-problems (concat leetcode--api-root "/problems/all/"))
+(defconst leetcode--api-all-tags (concat leetcode--base-url "/problems/api/tags/"))
+(defconst leetcode--api-daily-challenge
+ "query todayRecord { todayRecord { date question { questionFrontendId title titleSlug status } } }")
+;; submit
+(defconst leetcode--api-submit (concat leetcode--base-url "/problems/%s/submit/"))
+(defconst leetcode--api-problems-submission (concat leetcode--base-url "/problems/%s/submissions/"))
+(defconst leetcode--api-check-submission (concat leetcode--base-url "/submissions/detail/%s/check/"))
+;; try testcase
+(defconst leetcode--api-try (concat leetcode--base-url "/problems/%s/interpret_solution/"))
+(defconst leetcode--api-try-referer (concat leetcode--base-url "/problems/%s/"))
+
+(defun to-list (vec)
+ "Convert VEC to list."
+ (append vec '()))
+
+(defmacro dovec (spec &rest body)
+ "Loop over a vector.
+EVALUATE BODY with VAR bound to each element in VEC, in turn.
+SPEC is just like (VAR VEC [RESULT]). Then evaluate RESULT to
+get the return value (nil if RESULT is omitted).
+
+\(fn (VAR VEC [RESULT]) BODY...)"
+ (declare (indent 1))
+ (let ((start 0)
+ (counter (gensym))
+ (end (gensym)))
+ `(let ((,counter ,start)
+ (,(car spec) nil)
+ (,end (length ,(cadr spec))))
+ (while (< ,counter ,end)
+ (setq ,(car spec) (aref ,(cadr spec) ,counter))
+ ,@body
+ (setq ,counter (1+ ,counter)))
+ ,@(cddr spec))))
+
+(defun leetcode--referer (value)
+ "It will return an alist as the HTTP Referer Header.
+VALUE should be the referer."
+ (cons "Referer" value))
+
+(defun leetcode--maybe-csrf-token ()
+ "Return csrf token if it exists, otherwise return nil."
+ (if-let ((cookie (seq-find
+ (lambda (item)
+ (string= (aref item 1)
+ "csrftoken"))
+ (url-cookie-retrieve leetcode--domain "/" t))))
+ (aref cookie 2)))
+
+(aio-defun leetcode--csrf-token ()
+ "Return csrf token."
+ (unless (leetcode--maybe-csrf-token)
+ (aio-await (aio-url-retrieve leetcode--url-login)))
+ (leetcode--maybe-csrf-token))
+
+(defun leetcode--credentials ()
+ "Receive user account and password."
+ (let ((auth-source-creation-prompts
+ '((user . "LeetCode user: ")
+ (secret . "LeetCode password for %u: ")))
+ (found (car (auth-source-search
+ :max 1
+ :host leetcode--domain
+ :require '(:user :secret)
+ :create t))))
+ (if found
+ (list (plist-get found :user)
+ (let ((secret (plist-get found :secret)))
+ (if (functionp secret)
+ (funcall secret)
+ secret))
+ (plist-get found :save-function)))))
+
+(defun leetcode--multipart-form-data (name value)
+ "Generate multipart form data with NAME and VALUE."
+ `("file"
+ ("name" . ,name)
+ ("filedata" . ,value)
+ ("filename" . "")
+ ("content-type" . "")))
+
+(aio-defun leetcode--login ()
+ "Steal LeetCode login session from local browser.
+It also cleans LeetCode cookies in `url-cookie-file'."
+ (leetcode--loading-mode t)
+ (ignore-errors (url-cookie-delete-cookies leetcode--domain))
+ (aio-await (leetcode--csrf-token)) ;knock knock, whisper me the mysterious information
+ (let* ((my-cookies (executable-find "my_cookies"))
+ (my-cookies-output (shell-command-to-string (concat (shell-quote-argument my-cookies) " cn")))
+ (cookies-list (seq-filter
+ (lambda (s) (not (string-empty-p s)))
+ (split-string my-cookies-output "\n")))
+ (cookies-pairs (seq-map
+ (lambda (s) (split-string s))
+ cookies-list))
+ (leetcode-session (cadr (assoc "LEETCODE_SESSION" cookies-pairs)))
+ (leetcode-csrftoken (cadr (assoc "csrftoken" cookies-pairs))))
+ (leetcode--debug "login session: '%s'" leetcode-session)
+ (leetcode--debug "login csrftoken: '%s'" leetcode-csrftoken)
+ (url-cookie-store "LEETCODE_SESSION" leetcode-session nil leetcode--domain "/" t)
+ (url-cookie-store "csrftoken" leetcode-csrftoken nil leetcode--domain "/" t))
+ (aio-await (leetcode--csrf-token)) ;knock knock, whisper me the mysterious information
+ (leetcode--loading-mode -1))
+
+(defun leetcode--login-p ()
+ "Whether user is login."
+ (let ((username (plist-get leetcode--user :username)))
+ (and username
+ (not (string-empty-p username))
+ (seq-find
+ (lambda (item)
+ (string= (aref item 1)
+ "LEETCODE_SESSION"))
+ (url-cookie-retrieve leetcode--domain "/" t)))))
+
+(defun leetcode--set-user-and-problems (user-and-problems)
+ "Set `leetcode--user' and `leetcode--all-problems'.
+If user isn't login, only `leetcode--all-problems' will be set.
+USER-AND-PROBLEMS is an alist comes from
+`leetcode--api-all-problems'."
+ ;; user
+ (let-alist user-and-problems
+ (setq leetcode--user
+ (list :username .user_name
+ :solved .num_solved
+ :easy .ac_easy
+ :medium .ac_medium
+ :hard .ac_hard))
+ (leetcode--debug "set user: %s, solved %s in %s problems" .user_name .num_solved .num_total)
+ ;; problem list
+ (setq leetcode--all-problems
+ (list
+ :num .num_total
+ :tag "all"
+ :problems
+ (let* ((len .num_total)
+ (problems nil))
+ (dotimes (i len)
+ (let-alist (aref .stat_status_pairs i)
+ (leetcode--debug "frontend_question_id: %s, question_id: %s, title: %s, status: %s"
+ .stat.frontend_question_id .stat.question_id .stat.question__title .status)
+ (push (list
+ :status .status
+ :id .stat.question_id
+ :backend-id .stat.question_id
+ :frontend-id .stat.frontend_question_id
+ :title .stat.question__title
+ :title-slug .stat.question__title_slug
+ :acceptance (format
+ "%.1f%%"
+ (* 100
+ (/ (float .stat.total_acs)
+ .stat.total_submitted)))
+ :difficulty .difficulty.level
+ :paid-only (eq .paid_only t))
+ problems)))
+ problems)))))
+
+(defun leetcode--set-tags (all-tags)
+ "Set `leetcode--all-tags' and `leetcode--all-problems' with ALL-TAGS."
+ (let ((tags-table (make-hash-table :size 3200)))
+ (let-alist all-tags
+ (dolist (topic (to-list .topics))
+ (let-alist topic
+ ;; set leetcode--all-tags
+ (unless (member .slug leetcode--all-tags)
+ (push .slug leetcode--all-tags))
+ ;; tags-table cache
+ (dolist (id (to-list .questions))
+ (puthash id (cons .slug (gethash id tags-table)) tags-table)))))
+ ;; set problems tags with tags-table
+ (dolist (problem (plist-get leetcode--all-problems :problems))
+ (let ((backend-id (plist-get problem :backend-id)))
+ (plist-put problem :tags (gethash backend-id tags-table))))))
+
+(defun leetcode--slugify-title (title)
+ "Make TITLE a slug title.
+Such as 'Two Sum' will be converted to 'two-sum'."
+ (let* ((str1 (replace-regexp-in-string "[\s-]+" "-" (downcase title)))
+ (res (replace-regexp-in-string "[(),]" "" str1)))
+ res))
+
+(defun leetcode--problem-graphql-params (operation &optional vars)
+ "Construct a GraphQL parameter.
+OPERATION and VARS are LeetCode GraphQL parameters."
+ (list
+ (cons "operationName" operation)
+ (cons "query"
+ (graphql-query
+ problemsetQuestionList
+ (:arguments
+ (($titleSlug . String!))
+ (question
+ :arguments
+ ((titleSlug . ($ titleSlug)))
+ likes
+ dislikes
+ content
+ translatedContent
+ status
+ sampleTestCase
+ (topicTags slug)
+ (codeSnippets langSlug code)))))
+ (if vars (cons "variables" vars))))
+
+(aio-defun leetcode--fetch-problem (title)
+ "Fetch single problem.
+TITLE is a problem's title.
+Return a object with following attributes:
+:likes Number
+:dislikes Number
+:content String
+:topicTags String"
+ (let* ((slug-title (leetcode--slugify-title title))
+ (url-request-method "POST")
+ (url-request-extra-headers
+ `(,leetcode--User-Agent
+ ,(cons "Content-Type" "application/json")))
+ (url-request-data
+ (json-encode (leetcode--problem-graphql-params
+ "problemsetQuestionList"
+ (list (cons "titleSlug" slug-title)))))
+ (result (aio-await (aio-url-retrieve leetcode--api-graphql))))
+ (if-let ((error-info (plist-get (car result) :error)))
+ (progn
+ (switch-to-buffer (cdr result))
+ (leetcode--warn "LeetCode fetch problem ERROR: %S" error-info))
+ (with-current-buffer (cdr result)
+ (goto-char url-http-end-of-headers)
+ (let* ((json (json-read))
+ (question (alist-get 'question (alist-get 'data json))))
+ question)))))
+
+(defun leetcode--markdown-to-html (markdown-text)
+ "Convert simple Markdown to HTML for better rendering.
+Supports: **bold**, *italic*, - lists, > blockquotes, code blocks."
+ (let ((html-text markdown-text))
+ ;; Convert **text** to <strong>text</strong>
+ (setq html-text (replace-regexp-in-string "\\*\\*\\([^*]+\\)\\*\\*" "<strong>\\1</strong>" html-text))
+ ;; Convert *text* to <em>text</em>
+ (setq html-text (replace-regexp-in-string "\\*\\([^*]+\\)\\*" "<em>\\1</em>" html-text))
+ ;; Convert `text` to <code>text</code>
+ (setq html-text (replace-regexp-in-string "`\\([^`]+\\)`" "<code>\\1</code>" html-text))
+ ;; Convert newlines to <br/>
+ (setq html-text (replace-regexp-in-string "\n" "<br/>" html-text))
+ ;; Convert - list items (with indentation)
+ (setq html-text (replace-regexp-in-string "^- " "&nbsp;&nbsp;&nbsp;•&nbsp;" html-text))
+ ;; Convert > blockquotes to indented text
+ (setq html-text (replace-regexp-in-string "^> " "&nbsp;&nbsp;&nbsp;&nbsp;" html-text))
+ html-text))
+
+(defun leetcode--replace-in-buffer (regex to)
+ "Replace string matched REGEX in `current-buffer' to TO."
+ (with-current-buffer (current-buffer)
+ (save-excursion
+ (goto-char (point-min))
+ (save-match-data
+ (while (re-search-forward regex (point-max) t)
+ (replace-match to))))))
+
+(defun leetcode--make-tabulated-headers (header-names rows)
+ "Calculate headers width.
+Column width calculated by picking the max width of every cell
+under that column and the HEADER-NAMES. HEADER-NAMES are a list
+of header name, ROWS are a list of vector, each vector is one
+row."
+ (let ((widths
+ (seq-reduce
+ (lambda (acc row)
+ (cl-mapcar
+ (lambda (a col) (max a (length col)))
+ acc
+ (append row '())))
+ rows
+ (seq-map #'length header-names))))
+ (vconcat
+ (cl-mapcar
+ (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:
+\([<checkmark> <position> <title> <acceptance> <difficulty>] ...)"
+ (let ((problems (plist-get leetcode--all-problems :problems))
+ (easy-tag "easy")
+ (medium-tag "medium")
+ (hard-tag "hard")
+ rows)
+ (dolist (p problems)
+ (if (or leetcode--display-paid
+ (not (plist-get p :paid-only)))
+ (setq rows
+ (cons
+ (vector
+ ;; status
+ (if (equal (plist-get p :status) "ac")
+ (prog1 leetcode--checkmark
+ (put-text-property
+ 0 (length leetcode--checkmark)
+ 'font-lock-face 'leetcode-checkmark-face leetcode--checkmark))
+ " ")
+ ;; id
+ (number-to-string (plist-get p :id))
+ ;; title
+ (concat
+ (plist-get p :title)
+ " "
+ (if (eq (plist-get p :paid-only) t)
+ (prog1 leetcode--paid
+ (put-text-property
+ 0 (length leetcode--paid)
+ 'font-lock-face 'leetcode-paid-face leetcode--paid))
+ " "))
+ ;; acceptance
+ (plist-get p :acceptance)
+ ;; difficulty
+ (leetcode--stringify-difficulty (plist-get p :difficulty))
+ ;; tags
+ (if leetcode--display-tags (string-join (plist-get p :tags) ", ") ""))
+ rows))))
+ (reverse rows)))
+
+(defun leetcode--row-tags (row)
+ "Get tags from ROW."
+ (aref row 5))
+
+(defun leetcode--row-difficulty (row)
+ "Get difficulty from ROW."
+ (aref row 4))
+
+(defun leetcode--filter (rows)
+ "Filter ROWS by `leetcode--filter-regex', `leetcode--filter-tag' and `leetcode--filter-difficulty'."
+ (seq-filter
+ (lambda (row)
+ (and
+ (if leetcode--filter-regex
+ (let ((title (aref row 2)))
+ (string-match-p leetcode--filter-regex title))
+ t)
+ (if leetcode--filter-tag
+ (let ((tags (split-string (leetcode--row-tags row) ", ")))
+ (member leetcode--filter-tag tags))
+ t)
+ (if leetcode--filter-difficulty
+ (let ((difficulty (leetcode--row-difficulty row)))
+ (string= difficulty leetcode--filter-difficulty))
+ t)))
+ rows))
+
+(defun leetcode-reset-filter ()
+ "Reset filter."
+ (interactive)
+ (setq leetcode--filter-regex nil)
+ (setq leetcode--filter-tag nil)
+ (setq leetcode--filter-difficulty nil)
+ (leetcode-refresh))
+
+(defun leetcode-set-filter-regex (regex)
+ "Set `leetcode--filter-regex' as REGEX and refresh."
+ (interactive "sSearch: ")
+ (setq leetcode--filter-regex regex)
+ (leetcode-refresh))
+
+(defun leetcode-set-filter-tag ()
+ "Set `leetcode--filter-tag' from `leetcode--all-tags' and refresh."
+ (interactive)
+ (setq leetcode--filter-tag
+ (completing-read "Tags: " leetcode--all-tags))
+ (leetcode-refresh))
+
+(defun leetcode-set-prefer-language ()
+ "Set `leetcode-prefer-language' from `leetcode--lang-suffixes' and refresh."
+ (interactive)
+ (setq leetcode-prefer-language
+ (completing-read "Language: " leetcode--lang-suffixes))
+ (leetcode-refresh))
+
+(defun leetcode-set-filter-difficulty ()
+ "Set `leetcode--filter-difficulty' from `leetcode--all-difficulties' and refresh."
+ (interactive)
+ (setq leetcode--filter-difficulty
+ (completing-read "Difficulty: " leetcode--all-difficulties))
+ (leetcode-refresh))
+
+(defun leetcode-toggle-tag-display ()
+ "Toggle `leetcode--display-tags` and refresh"
+ (interactive)
+ (setq leetcode--display-tags (not leetcode--display-tags))
+ (leetcode-refresh))
+
+(defun leetcode-toggle-paid-display ()
+ "Toggle `leetcode--display-paid` and refresh"
+ (interactive)
+ (setq leetcode--display-paid (not leetcode--display-paid))
+ (leetcode-refresh))
+
+(aio-defun leetcode--fetch-all-tags ()
+ (let* ((url-request-method "GET")
+ (url-request-extra-headers
+ `(,leetcode--User-Agent
+ ,leetcode--X-Requested-With
+ ,(leetcode--referer leetcode--url-login)))
+ (result (aio-await (aio-url-retrieve leetcode--api-all-tags))))
+ (with-current-buffer (cdr result)
+ (goto-char url-http-end-of-headers)
+ (json-read))))
+
+(aio-defun leetcode--fetch-user-and-problems ()
+ "Fetch user and problems info."
+ (if leetcode--loading-mode
+ (message "LeetCode has been refreshing...")
+ (leetcode--loading-mode t)
+ (let ((url-request-method "GET")
+ (url-request-extra-headers
+ `(,leetcode--User-Agent
+ ,leetcode--X-Requested-With
+ ,(leetcode--referer leetcode--url-login)))
+ (result (aio-await (aio-url-retrieve leetcode--api-all-problems))))
+ (leetcode--loading-mode -1)
+ (if-let ((error-info (plist-get (car result) :error)))
+ (progn
+ (switch-to-buffer (cdr result))
+ (leetcode--warn "LeetCode fetch user and problems failed: %S" error-info))
+ (with-current-buffer (cdr result)
+ (goto-char url-http-end-of-headers)
+ (json-read))))))
+
+(defun leetcode-refresh ()
+ "Make `tabulated-list-entries'."
+ (interactive)
+ (let* ((header-names (append '(" " "#" "Problem" "Acceptance" "Difficulty")
+ (if leetcode--display-tags '("Tags"))))
+ (rows (leetcode--filter (leetcode--problems-rows)))
+ (headers (leetcode--make-tabulated-headers header-names rows)))
+ (with-current-buffer (get-buffer-create leetcode--buffer-name)
+ (leetcode--problems-mode)
+ (setq tabulated-list-format headers)
+ (setq tabulated-list-entries
+ (cl-mapcar
+ (lambda (i x) (list i x))
+ (number-sequence 0 (1- (length rows)))
+ rows))
+ (tabulated-list-init-header)
+ (tabulated-list-print t)
+ (leetcode--loading-mode -1))))
+
+(aio-defun leetcode-refresh-fetch ()
+ "Refresh problems and update `tabulated-list-entries'."
+ (interactive)
+ (if-let ((users-and-problems (aio-await (leetcode--fetch-user-and-problems)))
+ (all-tags (aio-await (leetcode--fetch-all-tags))))
+ (progn
+ (leetcode--set-user-and-problems users-and-problems)
+ (leetcode--set-tags all-tags))
+ (leetcode--warn "LeetCode parse user and problems failed"))
+ (setq leetcode--display-tags leetcode-prefer-tag-display)
+ (leetcode-reset-filter)
+ (leetcode-refresh))
+
+(aio-defun leetcode--async ()
+ "Show leetcode problems buffer."
+ (if (get-buffer leetcode--buffer-name)
+ (switch-to-buffer leetcode--buffer-name)
+ (unless (leetcode--login-p)
+ (aio-await (leetcode--login)))
+ (aio-await (leetcode-refresh-fetch))
+ (switch-to-buffer leetcode--buffer-name)))
+
+;;;###autoload
+(defun leetcode ()
+ "A wrapper for `leetcode--async', because emacs-aio can not be autoloaded.
+see: https://github.com/skeeto/emacs-aio/issues/3."
+ (interactive)
+ (if (leetcode--check-deps)
+ (leetcode--async)
+ (message "installing leetcode dependencies...")))
+
+;;;###autoload(autoload 'leetcode-daily "leetcode" nil t)
+(aio-defun leetcode-daily ()
+ "Open the daily challenge."
+ (interactive)
+ (unless (leetcode--login-p)
+ (aio-await (leetcode)))
+ (let* ((url-request-method "POST")
+ (url-request-extra-headers
+ `(,leetcode--User-Agent
+ ("Content-Type" . "application/json")
+ ,(leetcode--referer leetcode--url-login)
+ ,(cons leetcode--X-CSRFToken (leetcode--maybe-csrf-token))))
+ (url-request-data
+ (json-encode
+ `((operationName . "todayRecord")
+ (query . ,leetcode--api-daily-challenge)))))
+ (with-current-buffer (url-retrieve-synchronously leetcode--api-graphql)
+ (goto-char url-http-end-of-headers)
+ (let-alist (json-read)
+ (let ((qid .data.todayRecord.0.question.questionFrontendId))
+ (leetcode-show-problem (string-to-number qid)))))))
+
+(defun leetcode--buffer-content (buf)
+ "Get content without text properties of BUF."
+ (with-current-buffer buf
+ (buffer-substring-no-properties
+ (point-min) (point-max))))
+
+(defun leetcode--get-slug-title-before-try/submit (code-buf)
+ "Get slug title before try or submit with CODE-BUF.
+LeetCode require slug-title as the request parameters."
+ (with-current-buffer code-buf
+ (if leetcode-save-solutions
+ (file-name-base (cadr (split-string (buffer-name) "_")))
+ (file-name-base (buffer-name)))))
+
+(aio-defun leetcode-try ()
+ "Asynchronously test the code using customized testcase."
+ (interactive)
+ (leetcode--loading-mode t)
+ (aio-await (leetcode--csrf-token)) ;knock knock, whisper me the mysterious information
+ (leetcode--debug "csrf token: %s" (aio-await (leetcode--csrf-token)))
+ (let* ((code-buf (current-buffer))
+ (testcase-buf (get-buffer leetcode--testcase-buffer-name))
+ (slug-title (leetcode--get-slug-title-before-try/submit code-buf))
+ (problem (seq-find (lambda (p)
+ (equal slug-title
+ (leetcode--slugify-title
+ (plist-get p :title))))
+ (plist-get leetcode--all-problems :problems)))
+ (problem-id (plist-get problem :backend-id)))
+ (leetcode--debug "leetcode try slug-title: %s, problem-id: %s" slug-title problem-id)
+ (let* ((url-request-method "POST")
+ (url-request-extra-headers
+ `(,leetcode--User-Agent
+ ("Content-Type" . "application/json")
+ ,(leetcode--referer (format
+ leetcode--api-try-referer
+ slug-title))
+ ,(cons leetcode--X-CSRFToken (aio-await (leetcode--csrf-token)))))
+ (url-request-data
+ (json-encode
+ `((data_input . ,(leetcode--buffer-content testcase-buf))
+ (lang . ,leetcode--lang)
+ (question_id . ,problem-id)
+ (typed_code . ,(leetcode--buffer-content code-buf)))))
+ (result (aio-await (aio-url-retrieve (format leetcode--api-try slug-title)))))
+ (if-let ((error-info (plist-get (car result) :error)))
+ (progn
+ (switch-to-buffer (cdr result))
+ (leetcode--warn "LeetCode try failed: %S" error-info))
+ (let ((data (with-current-buffer (cdr result)
+ (goto-char url-http-end-of-headers)
+ (json-read)))
+ (res-buf (get-buffer leetcode--result-buffer-name)))
+ (let-alist data
+ (with-current-buffer res-buf
+ (erase-buffer)
+ (insert (concat "Your input:\n" .test_case "\n\n")))
+ ;; poll expected
+ (leetcode--debug "interpret_expected_id: %s" .interpret_expected_id)
+ (let ((expect_res (aio-await (leetcode--check-submission .interpret_expected_id slug-title)))
+ (retry-times 0))
+ (while (and (not expect_res) (< retry-times leetcode-retry-threshold))
+ (aio-await (aio-sleep 0.5))
+ (setq expect_res (aio-await (leetcode--check-submission .interpret_expected_id slug-title)))
+ (setq retry-times (1+ retry-times)))
+ (if (< retry-times leetcode-retry-threshold)
+ (let-alist expect_res
+ (with-current-buffer res-buf
+ (goto-char (point-max))
+ (cond
+ ((eq .status_code 10)
+ (insert "Expected:\n")
+ (dotimes (i (length .code_answer))
+ (insert (aref .code_answer i))
+ (insert "\n"))
+ (insert "\n"))
+ ((not (eq .status_code 10))
+ (insert "Expected:\nGot None\n"))
+ )))))
+
+ ;; poll interpreted
+ (let ((actual_res (aio-await (leetcode--check-submission .interpret_id slug-title)))
+ (retry-times 0))
+ (while (and (not actual_res) (< retry-times leetcode-retry-threshold))
+ (aio-await (aio-sleep 0.5))
+ (setq actual_res (aio-await (leetcode--check-submission .interpret_id slug-title)))
+ (setq retry-times (1+ retry-times)))
+ (if (< retry-times leetcode-retry-threshold)
+ (let-alist actual_res
+ (with-current-buffer res-buf
+ (goto-char (point-max))
+ (cond
+ ((eq .status_code 10)
+ (insert "Output:\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"))
+ (insert "\n"))
+ ((eq .status_code 14)
+ (insert .status_msg))
+ ((eq .status_code 15)
+ (insert .status_msg)
+ (insert "\n\n")
+ (insert .full_runtime_error))
+ ((eq .status_code 20)
+ (insert .status_msg)
+ (insert "\n\n")
+ (insert .full_compile_error)))
+ (when (> (length .code_output) 0)
+ (insert "\n\n")
+ (insert "Code output:\n")
+ (dolist (item (append .code_output nil))
+ (insert (concat item "\n"))))
+ (insert "\n\n")))
+ (leetcode--warn "LeetCode try timeout.")))
+ (leetcode--loading-mode -1)))))))
+
+(aio-defun leetcode--check-submission (submission-id slug-title)
+ "Polling to check submission detail.
+After each submission, either try testcase or submit, LeetCode
+returns a SUBMISSION-ID. With the SUBMISSION-ID, client will poll
+for the submission detail. SLUG-TITLE is a slugified problem
+title. Return response data if submission success, otherwise
+nil."
+ (leetcode--loading-mode t)
+ (let* ((url-request-method "GET")
+ (url-request-extra-headers
+ `(,leetcode--User-Agent
+ ,(leetcode--referer (format leetcode--api-problems-submission slug-title))
+ ,(cons leetcode--X-CSRFToken (aio-await (leetcode--csrf-token)))))
+ (result (aio-await (aio-url-retrieve (format leetcode--api-check-submission submission-id)))))
+ (if-let ((error-info (plist-get (car result) :error)))
+ (progn
+ (leetcode--loading-mode -1)
+ (switch-to-buffer (cdr result))
+ (leetcode--warn "LeetCode check submission failed: %S" error-info))
+ (with-current-buffer (cdr result)
+ (let ((submission-res
+ (progn (goto-char url-http-end-of-headers)
+ (json-read))))
+ (if (equal (alist-get 'state submission-res) "SUCCESS")
+ submission-res))))))
+
+(defun leetcode--solving-layout ()
+ "Specify layout for solving problem.
++---------------+----------------+
+| | |
+| | Description |
+| | |
+| +----------------+
+| Code | Customize |
+| | Testcases |
+| +----------------+
+| |Submit/Testcases|
+| | Result |
++---------------+----------------+"
+ (delete-other-windows)
+ (split-window-horizontally)
+ (other-window 1)
+ (split-window-below)
+ (other-window 1)
+ (split-window-below)
+ (other-window -1)
+ (other-window -1))
+
+(defun leetcode--display-result (buffer &optional alist)
+ "Display function for LeetCode result.
+BUFFER is the one to show LeetCode result. ALIST is a combined
+alist specified in `display-buffer-alist'."
+ (let ((window (window-next-sibling
+ (window-next-sibling
+ (window-top-child
+ (window-next-sibling
+ (window-left-child
+ (frame-root-window))))))))
+ (set-window-buffer window buffer)
+ window))
+
+(defun leetcode--display-testcase (buffer &optional alist)
+ "Display function for LeetCode testcase.
+BUFFER is the one to show LeetCode testcase. ALIST is a combined
+alist specified in `display-buffer-alist'."
+ (let ((window (window-next-sibling
+ (window-top-child
+ (window-next-sibling
+ (window-left-child
+ (frame-root-window)))))))
+ (set-window-buffer window buffer)
+ window))
+
+(defun leetcode--display-code (buffer &optional alist)
+ "Display function for LeetCode code.
+BUFFER is the one to show LeetCode code. ALIST is a combined
+alist specified in `display-buffer-alist'."
+ (let ((window (window-left-child (frame-root-window))))
+ (set-window-buffer window buffer)
+ window))
+
+(defun leetcode--show-submission-result (submission-detail)
+ "Show error info in `leetcode--result-buffer-name' based on status code.
+Error info comes from SUBMISSION-DETAIL. STATUS_CODE has
+following possible value:
+- 10: Accepted
+- 11: Wrong Anwser
+- 14: Time Limit Exceeded
+- 15: Runtime Error. full_runtime_error
+- 20: Compile Error. full_compile_error"
+ (let-alist submission-detail
+ (with-current-buffer (get-buffer-create leetcode--result-buffer-name)
+ (erase-buffer)
+ (insert (format "Status: %s" .status_msg))
+ (cond
+ ((eq .status_code 10)
+ (insert (format " (%s/%s)\n\n" .total_correct .total_testcases))
+ (insert (format "Runtime: %s, faster than %.2f%% of %s submissions.\n\n"
+ .status_runtime .runtime_percentile .pretty_lang))
+ (insert (format "Memory Usage: %s, less than %.2f%% of %s submissions."
+ .status_memory .memory_percentile .pretty_lang)))
+ ((eq .status_code 11)
+ (insert (format " (%s/%s)\n\n" .total_correct .total_testcases))
+ (insert (format "Test Case: \n%s\n\n" .input))
+ (insert (format "Answer: %s\n\n" .code_output))
+ (insert (format "Expected Answer: %s\n\n" .expected_output))
+ (insert (format "Stdout: \n%s\n" .std_output)))
+ ((eq .status_code 14)
+ (insert "\n"))
+ ((eq .status_code 15)
+ (insert "\n\n")
+ (insert (format (alist-get 'full_runtime_error submission-detail))))
+ ((eq .status_code 20)
+ (insert "\n\n")
+ (insert (format (alist-get 'full_compile_error submission-detail)))))
+ (display-buffer (current-buffer)
+ '((display-buffer-reuse-window
+ leetcode--display-result)
+ (reusable-frames . visible))))))
+
+(aio-defun leetcode-submit ()
+ "Asynchronously submit the code and show result."
+ (interactive)
+ (leetcode--loading-mode t)
+ (let* ((code-buf (current-buffer))
+ (code (leetcode--buffer-content code-buf))
+ (slug-title (leetcode--get-slug-title-before-try/submit code-buf))
+ (problem-id (plist-get (seq-find (lambda (p)
+ (equal slug-title
+ (leetcode--slugify-title
+ (plist-get p :title))))
+ (plist-get leetcode--all-problems :problems))
+ :backend-id)))
+ (leetcode--debug "leetcode submit slug-title: %s, problem-id: %s" slug-title problem-id)
+ (let* ((url-request-method "POST")
+ (url-request-extra-headers
+ `(,leetcode--User-Agent
+ ,(leetcode--referer (format
+ leetcode--api-problems-submission
+ slug-title))
+ ,(cons "Content-Type" "application/json")
+ ,(cons leetcode--X-CSRFToken (aio-await (leetcode--csrf-token)))))
+ (url-request-data
+ (json-encode `((lang . ,leetcode--lang)
+ (question_id . ,problem-id)
+ (typed_code . ,code))))
+ (result (aio-await (aio-url-retrieve (format leetcode--api-submit slug-title)))))
+ (if-let ((error-info (plist-get (car result) :error)))
+ (progn
+ (leetcode--loading-mode -1)
+ (switch-to-buffer (cdr result))
+ (leetcode--warn "LeetCode check submit failed: %S" error-info))
+ (let* ((resp
+ (with-current-buffer (cdr result)
+ (progn (goto-char url-http-end-of-headers)
+ (json-read))))
+ (submission-id (alist-get 'submission_id resp))
+ (submission-res (aio-await (leetcode--check-submission submission-id slug-title)))
+ (retry-times 0))
+ ;; poll submission result
+ (while (and (not submission-res) (< retry-times leetcode-retry-threshold))
+ (aio-await (aio-sleep 0.5))
+ (setq submission-res (aio-await (leetcode--check-submission submission-id slug-title)))
+ (setq retry-times (1+ retry-times)))
+ (if (< retry-times leetcode-retry-threshold)
+ (leetcode--show-submission-result submission-res)
+ (leetcode--warn "LeetCode submit timeout."))
+ (leetcode--loading-mode -1))))))
+
+(defun leetcode--problem-link (title)
+ "Generate problem link from TITLE."
+ (concat leetcode--base-url "/problems/" (leetcode--slugify-title 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))))
+ (let* ((content-val (alist-get 'content problem))
+ (translated-val (alist-get 'translatedContent problem))
+ (likes-val (alist-get 'likes problem))
+ (dislikes-val (alist-get 'dislikes problem))
+ (desc-content (if (and content-val
+ (string-match-p "English description is not available" content-val))
+ translated-val
+ content-val)))
+ (with-temp-buffer
+ (insert (concat "<h1>" (number-to-string problem-id) ". " title "</h1>"))
+ (insert (concat (capitalize difficulty) html-margin
+ "likes: " (number-to-string likes-val) html-margin
+ "dislikes: " (number-to-string dislikes-val)))
+ (insert "<hr/>")
+ ;; Convert markdown to HTML
+ (let ((converted (leetcode--markdown-to-html (or desc-content ""))))
+ (insert converted))
+ (setq shr-current-font t)
+ ;; NOTE: shr.el can't render "https://xxxx.png", so we use "http"
+ (leetcode--replace-in-buffer "https" "http")
+ (shr-render-buffer (current-buffer)))
+ (with-current-buffer "*html*"
+ (save-match-data
+ (re-search-forward "dislikes: .*" nil t)
+ (insert (make-string 4 ?\s))
+ (insert-text-button "Solve it"
+ 'action (lambda (btn)
+ (leetcode--start-coding problem problem-info))
+ 'help-echo "solve the problem.")
+ (insert (make-string 4 ?\s))
+ (insert-text-button "Link"
+ 'action (lambda (btn)
+ (browse-url (leetcode--problem-link title)))
+ 'help-echo "open the problem in browser.")
+ (insert (make-string 4 ?\s))
+ (insert-text-button "Solution"
+ 'action (lambda (btn)
+ (browse-url (concat (leetcode--problem-link title) "/solution")))
+ 'help-echo "Open the problem solution page in browser.")
+ ;; Would be best to parse the solution in Emacs, but the url-retrieve-synchronously only get the Javascript which generate the solution in HTML, not directly text
+ )
+ (rename-buffer buf-name)
+ (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-slug (or (plist-get problem-info :title-slug)
+ (leetcode--slugify-title (plist-get problem-info :title))))
+ (problem (aio-await (leetcode--fetch-problem title-slug))))
+ (leetcode--show-problem problem problem-info)))
+
+(defun leetcode-show-problem-by-slug (slug-title)
+ "Show the description of problem with slug title. This function will work after first run M-x leetcode. This can be used with org-link elisp:(leetcode-show-problem-by-slug \"two-sum\").
+Get problem by slug title and use `shr-render-buffer' to render problem
+description. This action will show the description in other
+window and jump to it."
+ (interactive (list (completing-read "Problem slug (e.g., two-sum): "
+ (mapcar (lambda (p)
+ (leetcode--slugify-title (plist-get p :title)))
+ (plist-get leetcode--all-problems :problems)))))
+ (let* ((problem (seq-find (lambda (p)
+ (equal slug-title
+ (leetcode--slugify-title
+ (plist-get p :title))))
+ (plist-get leetcode--all-problems :problems)))
+ (problem-id (plist-get problem :id))
+ (problem-info (leetcode--get-problem-by-id problem-id))
+ (title-slug (or (plist-get problem-info :title-slug)
+ (leetcode--slugify-title (plist-get problem-info :title))))
+ (problem (leetcode--fetch-problem title-slug)))
+ (leetcode-show-problem problem-id)))
+
+(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-slug (or (plist-get problem-info :title-slug)
+ (leetcode--slugify-title (plist-get problem-info :title))))
+ (problem (aio-await (leetcode--fetch-problem title-slug))))
+ (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)
+ (kill-buffer buf))
+
+(defun leetcode-quit ()
+ "Close and delete leetcode related buffers and windows."
+ (interactive)
+ (leetcode--kill-buff-and-delete-window (get-buffer leetcode--buffer-name))
+ (leetcode--kill-buff-and-delete-window (get-buffer leetcode--description-buffer-name))
+ (leetcode--kill-buff-and-delete-window (get-buffer leetcode--result-buffer-name))
+ (leetcode--kill-buff-and-delete-window (get-buffer leetcode--testcase-buffer-name))
+ (mapc (lambda (title)
+ (leetcode--kill-buff-and-delete-window
+ (get-buffer (leetcode--get-code-buffer-name title))))
+ leetcode--problem-titles))
+
+(defcustom leetcode-prefer-tag-display t
+ "Whether to display tags by default in the *leetcode* buffer."
+ :type :boolean)
+
+;;(defcustom leetcode-use-cn-source nil
+;; "Whether to use leetcode-cn.com source."
+;; :type 'boolean
+;; :group 'leetcode)
+
+(defvar leetcode--display-tags leetcode-prefer-tag-display
+ "(Internal) Whether tags are displayed the *leetcode* buffer.")
+
+(defvar leetcode--display-paid nil
+ "(Internal) Whether paid problems are displayed the *leetcode* buffer.")
+
+(defvar leetcode-prefer-language "python3"
+ "LeetCode programming language.
+c, cpp, csharp, golang, java, javascript, typescript, kotlin, php, python,
+python3, ruby, rust, scala, swift.")
+
+(defvar leetcode-prefer-sql "mysql"
+ "LeetCode sql implementation.
+mysql, mssql, oraclesql.")
+
+(defvar leetcode-directory "~/leetcode"
+ "Directory to save solutions.")
+
+(defvar leetcode-save-solutions nil
+ "If it's t, save leetcode solutions to `leetcode-directory'.")
+
+(defvar leetcode--lang leetcode-prefer-language
+ "LeetCode programming language or sql for current problem internally.
+Default is programming language.")
+
+(defconst leetcode--lang-suffixes
+ '(("c" . ".c") ("cpp" . ".cpp") ("csharp" . ".cs")
+ ("golang" . ".go") ("java" . ".java") ("javascript" . ".js")
+ ("typescript" . ".ts") ("kotlin" . ".kt") ("php" . ".php")
+ ("python" . ".py") ("python3" . ".py") ("ruby" . ".rb")
+ ("rust" . ".rs") ("scala" . ".scala") ("swift" . ".swift")
+ ("mysql" . ".sql") ("mssql" . ".sql") ("oraclesql" . ".sql"))
+ "LeetCode programming language suffixes.
+c, cpp, csharp, golang, java, javascript, typescript, kotlin, php, python,
+python3, ruby, rust, scala, swift, mysql, mssql, oraclesql.")
+
+(defun leetcode--set-lang (snippets)
+ "Set `leetcode--lang' based on langSlug in SNIPPETS."
+ (setq leetcode--lang
+ (if (seq-find (lambda (s)
+ (equal (alist-get 'langSlug s)
+ leetcode-prefer-sql))
+ snippets)
+ leetcode-prefer-sql
+ leetcode-prefer-language)))
+
+(defun leetcode--get-code-buffer-name (title)
+ "Get code buffer name by TITLE and `leetcode-prefer-language'."
+ (let* ((suffix (assoc-default
+ leetcode--lang
+ leetcode--lang-suffixes))
+ (slug-title (leetcode--slugify-title title))
+ (title-with-suffix (concat slug-title suffix)))
+ (if leetcode-save-solutions
+ (format "%04d_%s" (leetcode--get-problem-id slug-title) title-with-suffix)
+ title-with-suffix)))
+
+(defun leetcode--get-code-buffer (buf-name)
+ "Get code buffer by BUF-NAME."
+ (if (not leetcode-save-solutions)
+ (get-buffer-create buf-name)
+ (unless (file-directory-p leetcode-directory)
+ (make-directory leetcode-directory))
+ (find-file-noselect
+ (concat (file-name-as-directory leetcode-directory)
+ buf-name))))
+
+(defun leetcode--get-problem (slug-title)
+ "Get problem from `leetcode--all-problems' by SLUG-TITLE."
+ (seq-find (lambda (p)
+ (equal slug-title
+ (leetcode--slugify-title
+ (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."
+ (seq-find (lambda (p)
+ (equal id (plist-get p :id)))
+ (plist-get leetcode--all-problems :problems)))
+
+(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--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 PROBLEM with meta-data PROBLEM-INFO.
+The buffer will be not associated with any file. It will choose
+major mode by `leetcode-prefer-language'and `auto-mode-alist'."
+ (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))
+ (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)
+ (define-key map "l" #'leetcode-set-prefer-language)
+ (define-key map "t" #'leetcode-set-filter-tag)
+ (define-key map "T" #'leetcode-toggle-tag-display)
+ (define-key map "P" #'leetcode-toggle-paid-display)
+ (define-key map "d" #'leetcode-set-filter-difficulty)
+ (define-key map "g" #'leetcode-refresh)
+ (define-key map "G" #'leetcode-refresh-fetch)
+ (define-key map "/" #'leetcode-reset-filter)
+ (define-key map "q" #'quit-window)))
+ "Keymap for `leetcode--problems-mode'.")
+
+(define-derived-mode leetcode--problems-mode
+ tabulated-list-mode "LC Problems"
+ "Major mode for browsing a list of problems."
+ (setq tabulated-list-padding 2)
+ (add-hook 'tabulated-list-revert-hook #'leetcode-refresh nil t)
+ :group 'leetcode)
+
+(add-hook 'leetcode--problems-mode-hook #'hl-line-mode)
+
+(defvar leetcode--problem-description-mode-map
+ (let ((map (make-sparse-keymap)))
+ (prog1 map
+ (suppress-keymap map)
+ (define-key map "n" #'next-line)
+ (define-key map "p" #'previous-line)))
+ "Keymap for `leetcode--problem-description-mode'.")
+
+(define-derived-mode leetcode--problem-description-mode
+ special-mode "LC Descri"
+ "Major mode for display problem description."
+ :group 'leetcode)
+
+;;; Use spinner.el to show progress indicator
+(defvar leetcode--spinner (spinner-create 'progress-bar-filled)
+ "Progress indicator to show request progress.")
+(defconst leetcode--loading-lighter
+ '(" [LeetCode" (:eval (spinner-print leetcode--spinner)) "]"))
+
+(define-minor-mode leetcode--loading-mode
+ "Minor mode to showing leetcode loading status."
+ :require 'leetcode
+ :lighter leetcode--loading-lighter
+ :group 'leetcode
+ (if leetcode--loading-mode
+ (spinner-start leetcode--spinner)
+ (spinner-stop leetcode--spinner)))
+
+(provide 'packages-leetcode)
+;;; leetcode.el ends here