diff options
| author | verdant <i@glowisle.me> | 2026-04-25 10:45:42 +0800 |
|---|---|---|
| committer | verdant <i@glowisle.me> | 2026-04-25 10:45:42 +0800 |
| commit | 6c38d42f1a17fee4d9ead9d146e88620cf780ec7 (patch) | |
| tree | cbe6b096873bdf798b04961ba1096af848f78149 /packages | |
| parent | 07ea5504c9209239700f2aff3590d8d3f5cb5247 (diff) | |
| download | vemacs-6c38d42f1a17fee4d9ead9d146e88620cf780ec7.tar.gz vemacs-6c38d42f1a17fee4d9ead9d146e88620cf780ec7.zip | |
Initial commit
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/.hugo_build.lock | 0 | ||||
| -rw-r--r-- | packages/packages-editing.el | 95 | ||||
| -rw-r--r-- | packages/packages-email.el | 153 | ||||
| -rw-r--r-- | packages/packages-leetcode.el | 1386 | ||||
| -rw-r--r-- | packages/packages-misc.el | 8 | ||||
| -rw-r--r-- | packages/packages-ui.el | 41 | ||||
| -rw-r--r-- | packages/packages.el | 14 |
7 files changed, 1697 insertions, 0 deletions
diff --git a/packages/.hugo_build.lock b/packages/.hugo_build.lock new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/packages/.hugo_build.lock diff --git a/packages/packages-editing.el b/packages/packages-editing.el new file mode 100644 index 0000000..c2e8b5c --- /dev/null +++ b/packages/packages-editing.el @@ -0,0 +1,95 @@ +;; -*- lexical-binding: t; -*- +(use-package evil + :init + (setq evil-want-keybinding nil) + :config + (evil-mode) + :ensure t) + +(use-package evil-collection + :after evil + :ensure t + :config + (evil-collection-init)) + +(use-package olivetti) + +(use-package yasnippet + :ensure t + :config + (yas-global-mode 1)) + +(use-package ivy + :ensure t + :init + (ivy-mode 1) + (counsel-mode 1) + :config + (setq ivy-use-selectable-prompt t) + (setq ivy-use-preview t) + (setq ivy-fixed-height-minibuffer t) + (setq ivy-use-preview t) + (setq ivy-use-virtual-buffers t) + (setq search-default-mode #'char-fold-to-regexp) + (setq ivy-count-format "(%d/%d) ") + (setq ivy-initial-inputs-alist nil) ;; 不预设初始输入 + (setq ivy-use-selectable-prompt t) ;; 允许选择提示 + :bind + (("C-s" . 'swiper) + ("C-x b" . 'ivy-switch-buffer) + ("C-c v" . 'ivy-push-view) + ("C-c s" . 'ivy-switch-view) + ("C-c V" . 'ivy-pop-view) + ("C-x C-@" . 'counsel-mark-ring); 在某些终端上 C-x C-SPC 会被映射为 C-x C-@,比如在 macOS 上,所以要手动设置 + ("C-x C-SPC" . 'counsel-mark-ring) + :map minibuffer-local-map + ("C-r" . counsel-minibuffer-history))) + +;; 启用 company-mode 全局补全 +(use-package company + :ensure t + :config + (setq company-idle-delay 0.0 + company-minimum-prefix-length 1) + (global-company-mode) + + (with-eval-after-load 'company + ;; 补全列表背景 + (set-face-attribute 'company-tooltip nil + :foreground "white" :background "gray20") + ;; 选中项背景 + (set-face-attribute 'company-tooltip-selection nil + :foreground "blue" :background "gray20") + ;; 输入前缀高亮 + (set-face-attribute 'company-tooltip-common nil + :foreground "orange" :background "gray20") + ;; 右侧注释/类型 + (set-face-attribute 'company-tooltip-annotation nil + :foreground "cyan" :background "gray20"))) + +(use-package lsp-mode + :ensure t + :init + (setq read-process-output-max (* 1024 1024)) + :hook ( + (c-mode . lsp) + (go-mode .lsp) + (css-mode . lsp) + (html-mode . lsp)) + ) + +(use-package lsp-ui + :ensure t + :commands lsp-ui-mode) + +(use-package clang-format + :ensure t + :hook + (c-mode-common-hook . (lambda () (add-hook 'before-save-hook 'clang-format-buffer nil t))) + :bind + (:map c-mode-base-map + ("C-c C-f" . clang-format-buffer)) + :config + (setq clang-format-executable (executable-find "clang-format"))) + +(provide 'packages-editing) diff --git a/packages/packages-email.el b/packages/packages-email.el new file mode 100644 index 0000000..a519c42 --- /dev/null +++ b/packages/packages-email.el @@ -0,0 +1,153 @@ +;; -*- lexical-binding: t; -*- + +(require 'smtpmail) + +(add-to-list 'load-path "/usr/share/emacs/site-lisp/elpa-src/mu4e-1.8.14") + +(setq gnutls-algorithm-priority "NORMAL:%COMPAT") + +(defun mu4e-goodies~break-cjk-word (word) + "Break CJK word into list of bi-grams like: 我爱你 -> 我爱 爱你" + (if (or (<= (length word) 2) + (equal (length word) (string-bytes word))) + word + (let ((pos nil) + (char-list nil) + (br-word nil)) + (if (setq pos (string-match ":" word)) + (concat (substring word 0 (+ 1 pos)) + (mu4e-goodies~break-cjk-word (substring word (+ 1 pos)))) + (if (memq 'ascii (find-charset-string word)) + word + (progn + (setq char-list (split-string word "" t)) + (while (cdr char-list) + (setq br-word (concat br-word (concat (car char-list) (cadr char-list)) " ")) + (setq char-list (cdr char-list))) + br-word)))))) + +(defun mu4e-goodies~break-cjk-query (expr) + "Break CJK strings into bi-grams in query." + (let ((word-list (split-string expr " " t)) + (new "")) + (dolist (word word-list new) + (setq new (concat new (mu4e-goodies~break-cjk-word word) " "))))) + +(setq mu4e-query-rewrite-function 'mu4e-goodies~break-cjk-query) + +(use-package mu4e + :ensure nil + :if (executable-find "mu") + :commands (mu4e) + :bind (:map mu4e-view-mode-map + ("9" . scroll-down-command) + ("0" . scroll-up-command) + :map mu4e-search-minor-mode-map + ("/" . mu4e-search-maildir) + :map mu4e-main-mode-map + ("g" . mu4e-update-mail-and-index) + :map mu4e-headers-mode-map + ("<backspace>" . scroll-down-command) + ("j" . mu4e-headers-next) + ("k" . mu4e-headers-prev) + ("r" . mu4e-headers-mark-for-read) + ("!" . mu4e-headers-flag-all-read) + ("f" . mu4e-headers-mark-for-flag)) + + :custom + (mu4e-headers-fields '((:human-date . 12) + (:flags . 6) + (:from-or-to . 22) + (:thread-subject . nil))) + (mu4e-view-fields '(:from :to :cc :bcc :subject :flags + :date :maildir :mailing-list :tags)) + (mu4e-modeline-show-global nil) + (mu4e-hide-index-messages t) + + :init + (setq user-mail-address "im@verdant.ee" + user-full-name "Verdant" + mu4e-debug t) + + (setq message-send-mail-function 'sendmail-send-it + sendmail-program "/usr/bin/msmtp" + mail-specify-envelope-from t + mail-envelope-from 'header) + + (setq message-citation-line-format "\nOn %a, %b %d, %Y at %r %z, %N wrote:\n" + message-citation-line-function 'message-insert-formatted-citation-line + mm-discouraged-alternatives '("text/html" "text/richtext") + gnus-article-time-format "%a, %Y-%m-%d %T %z" + gnus-article-date-headers '(user-defined original)) + + :config + (require 'mu4e-contrib) + + (setq mail-user-agent 'mu4e-user-agent) + + (setq mu4e-contexts + (list + (make-mu4e-context + :name "Verdant" + :match-func (lambda (msg) + (when msg + (string-prefix-p "/ljc" (mu4e-message-field msg :maildir)))) + :vars '((mu4e-sent-folder . "/Verdant/Sent") + (mu4e-trash-folder . "/Verdant/Trash") + (mu4e-refile-folder . "/Verdant/Archive") + (mu4e-drafts-folder . "/Verdant/Drafts") + (user-mail-address . "im@verdant.ee"))))) + + (setq mu4e-compose-complete-only-personal t + mu4e-view-show-addresses t + mu4e-view-show-images nil + mu4e-attachment-dir "~/Downloads" + mu4e-sent-messages-behavior 'sent + mu4e-context-policy 'pick-first + mu4e-compose-context-policy 'ask-if-none + mu4e-compose-dont-reply-to-self t + mu4e-confirm-quit nil + mu4e-headers-date-format "%+4Y-%m-%d" + mu4e-view-html-plaintext-ratio-heuristic most-positive-fixnum + mu4e-update-interval (* 30 60) + mu4e-get-mail-command "true" + mu4e-compose-format-flowed t + mu4e-completing-read-function 'ido-completing-read) + + (setq mu4e-bookmarks '((:name "All Inbox" + :query "maildir:/Verdant/INBOX" + :key ?i) + (:name "Unread messages" + :query "flag:unread AND NOT flag:trashed" + :key ?u) + (:name "Today's messages" + :query "date:today..now AND NOT flag:trashed" + :key ?t) + (:name "Last 7 days" + :query "date:7d..now AND NOT flag:trashed" + :hide-unread t + :key ?w) + (:name "Flagged" + :query "flag:flagged" + :key ?f) + (:name "Sent" + :query "maildir:/Verdant/Sent" + :key ?s))) + + (add-to-list 'mu4e-view-actions '("browser" . mu4e-action-view-in-browser) t) + + (defun my/mu4e-pre-update-hook () + (let ((inhibit-message t)) + (message "Update and index mu4e at %s" (format-time-string "%D %-I:%M %p")))) + + (defun my/mu4e-stop-update-task () + (interactive) + (when mu4e--update-timer + (cancel-timer mu4e--update-timer) + (setq mu4e--update-timer nil))) + + (setq mu4e-update-pre-hook 'my/mu4e-pre-update-hook) + + (add-to-list 'mu4e-view-fields :bcc)) + +(provide 'packages-email) 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 "^- " " • " html-text)) + ;; Convert > blockquotes to indented text + (setq html-text (replace-regexp-in-string "^> " " " 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 " ")) + (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 diff --git a/packages/packages-misc.el b/packages/packages-misc.el new file mode 100644 index 0000000..baa7537 --- /dev/null +++ b/packages/packages-misc.el @@ -0,0 +1,8 @@ +;; -*- lexical-binding: t; -*- +(use-package hugoista + :ensure t + :custom + (hugoista-site-dir "~/blog") + (hugoista-hugo-command "~/go/bin/hugo")) + +(provide 'packages-misc) diff --git a/packages/packages-ui.el b/packages/packages-ui.el new file mode 100644 index 0000000..7f7e303 --- /dev/null +++ b/packages/packages-ui.el @@ -0,0 +1,41 @@ +;; -*- lexical-binding: t; y-*- +(use-package ace-window + :ensure t + :bind + (("C-x o" . ace-window))) + +(use-package counsel + :ensure t) + +(use-package dashboard + :ensure t + :config + (setq dashboard-startup-banner 'logo + dashboard-banner-logo-title "Welcome to Verdant's Emacverse!!!" + dashboard-center-content t + dashboard-set-heading-icons t + dashboard-items '((recents . 10) + (bookmarks . 5)) + dashboard-footer-messages '("verdant.el")) + + ;; 核心三件套 + (setq initial-buffer-choice (lambda () (get-buffer-create "*dashboard*"))) + (dashboard-setup-startup-hook) + (add-hook 'after-init-hook #'dashboard-open t)) + +(use-package ivy-posframe + :ensure t + :config + (ivy-posframe-mode t)) + +(use-package ivy-rich + :ensure t + :config + (ivy-rich-mode t)) + +(use-package diredfl + :ensure t + :hook (dired-mode . diredfl-mode)) + + +(provide 'packages-ui) diff --git a/packages/packages.el b/packages/packages.el new file mode 100644 index 0000000..ea0032b --- /dev/null +++ b/packages/packages.el @@ -0,0 +1,14 @@ +;; -*- lexical-binding: t; -*- +(require 'package) +(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) +(add-to-list 'package-archives '("gnu" . "https://https://elpa.gnu.org/packages/") t) +(add-to-list 'package-archives '("melpa" . "https://mirrors.ustc.edu.cn/elpa/melpa/") t) +(add-to-list 'package-archives '("nongnu" . "https://mirrors.ustc.edu.cn/elpa/nongnu/") t) +(package-initialize) + +(require 'packages-editing) +(require 'packages-ui) +(require 'packages-email) +(require 'packages-misc) +;; (require 'packages-leetcode) +(provide 'packages) |
