From c6be6f7027b2f309866e8e5189c35fb2f0b7d593 Mon Sep 17 00:00:00 2001 From: Max Amundsen Date: Fri, 26 Dec 2025 17:16:44 -0500 Subject: [PATCH] update emac --- .emacs.d/init.el | 212 ++++++++++++++++++++++++++++++++++-- .emacs.d/lisp/simpc-mode.el | 127 +++++++++++++++++++++ sync.sh | 53 +++++++++ 3 files changed, 385 insertions(+), 7 deletions(-) create mode 100644 .emacs.d/lisp/simpc-mode.el diff --git a/.emacs.d/init.el b/.emacs.d/init.el index bf4fc94..17ed3b1 100755 --- a/.emacs.d/init.el +++ b/.emacs.d/init.el @@ -23,13 +23,112 @@ (require 's) (require 'dash) (require 'popup) +(require 'simpc-mode) +;; Automatically enabling simpc-mode on files with extensions like .h, .c, .cpp, .hpp +(add-to-list 'auto-mode-alist '("\\.[hc]\\(pp\\)?\\'" . simpc-mode)) ;; ctags for xref, with dumb-jump as fallback (require 'dumb-jump) (setq dumb-jump-force-searcher 'grep) +(setq dumb-jump-selector 'ivy) (setq xref-show-definitions-function #'xref-show-definitions-completing-read) (setq tags-revert-without-query t) (add-hook 'xref-backend-functions #'dumb-jump-xref-activate 100) +;; eglot LSP client +;; When eglot is active, xref commands (F12, M-f12) automatically use LSP +;; instead of CTAGS. Eglot registers itself as a higher-priority xref backend. +(require 'eglot) +(setq eglot-autoshutdown t) ; shutdown server when last buffer is closed +(setq eglot-confirm-server-initiated-edits nil) ; don't ask for confirmation on renames + +;; disable LSP visual indicators but keep diagnostics available +(add-to-list 'eglot-ignored-server-capabilities :documentHighlightProvider) + +;; auto-confirm "modified buffer" prompts when jumping from diagnostics +(defun my-auto-confirm-modified-buffer (orig-fun prompt) + "Auto-confirm prompts about modified buffers having wrong location." + (if (string-match-p "has been modified.*wrong location" prompt) + t + (funcall orig-fun prompt))) +(advice-add 'y-or-n-p :around #'my-auto-confirm-modified-buffer) +;; hide squiggly underlines +(set-face-attribute 'flymake-error nil :underline nil) +(set-face-attribute 'flymake-warning nil :underline nil) +(set-face-attribute 'flymake-note nil :underline nil) +;; hide fringe indicators +(setq flymake-fringe-indicator-position nil) + +;; Go: auto-start eglot (requires gopls: go install golang.org/x/tools/gopls@latest) +(add-hook 'go-mode-hook 'eglot-ensure) + +(global-set-key (kbd "") 'eglot-rename) +(global-set-key (kbd "C-S-m") 'my-flymake-show-project-errors-warnings) +(global-set-key (kbd "") 'eglot-reconnect) +(global-set-key (kbd "C-t") 'my-lsp-find-workspace-symbol) + +(defun my-flymake-show-project-errors-warnings () + "Show project diagnostics, filtering out notes (only errors and warnings)." + (interactive) + ;; Kill existing diagnostics buffer to force refresh + (when-let ((buf (get-buffer "*Flymake diagnostics*"))) + (kill-buffer buf)) + ;; Refresh flymake on all project buffers + (dolist (buf (buffer-list)) + (with-current-buffer buf + (when (and (bound-and-true-p flymake-mode) + (buffer-file-name)) + (flymake-start)))) + (flymake-show-project-diagnostics) + (run-at-time 0.1 nil + (lambda () + (when-let ((buf (get-buffer "*Flymake diagnostics*"))) + (with-current-buffer buf + (let ((inhibit-read-only t)) + (goto-char (point-min)) + (while (not (eobp)) + (if (looking-at ".*\\bnote\\b.*$") + (delete-region (line-beginning-position) (1+ (line-end-position))) + (forward-line 1))))))))) + +(defun my-lsp-find-workspace-symbol () + "Interactively search for symbols in workspace using LSP." + (interactive) + (if (eglot-managed-p) + (let ((server (eglot-current-server)) + (root (project-root (project-current)))) + (ivy-read "Symbol: " + (lambda (input) + (when (and input (>= (length input) 1)) + (condition-case nil + (let* ((resp (jsonrpc-request server :workspace/symbol + `(:query ,input))) + (items (append resp nil))) + (delq nil + (mapcar (lambda (item) + (condition-case nil + (let* ((name (plist-get item :name)) + (loc (plist-get item :location)) + (uri (plist-get loc :uri)) + (range (plist-get loc :range)) + (start (plist-get range :start)) + (line (1+ (plist-get start :line))) + (file (eglot-uri-to-path uri)) + (rel-path (file-relative-name file root))) + (propertize (format "%s %s:%d" name rel-path line) + 'file file + 'line line)) + (error nil))) + items))) + (error nil)))) + :dynamic-collection t + :require-match t + :action (lambda (candidate) + (when (and candidate (get-text-property 0 'file candidate)) + (find-file (get-text-property 0 'file candidate)) + (goto-char (point-min)) + (forward-line (1- (get-text-property 0 'line candidate))))))) + (call-interactively 'xref-find-apropos))) + ;; vundo for visual undo tree (require 'vundo) (ivy-mode 1) @@ -61,6 +160,11 @@ (setq-local tab-width 4) (setq-local stupid-indent-level 4))) +(add-hook 'simpc-mode-hook (lambda () + (setq-local indent-tabs-mode t) + (setq-local tab-width 4) + (setq-local stupid-indent-level 4))) + (add-hook 'python-mode-hook (lambda () (setq-local indent-tabs-mode nil) (setq-local tab-width 4) @@ -81,9 +185,10 @@ ;; bottom panel settings (compilation, xref, etc.) (setq compilation-scroll-output -1) +(setq compilation-save-buffers-predicate 'ignore) ;; bottom panel buffer patterns -(defvar my-bottom-panel-buffers '("\\*compilation\\*" "\\*xref\\*") +(defvar my-bottom-panel-buffers '("\\*compilation\\*" "\\*xref\\*" "\\*Flymake diagnostics.*\\*") "List of buffer name patterns for bottom panel.") (defun my-bottom-panel-buffer-p (buf) @@ -118,6 +223,8 @@ '("\\*compilation\\*" (my-display-in-bottom-panel) (window-height . 0.25))) (add-to-list 'display-buffer-alist '("\\*xref\\*" (my-display-in-bottom-panel) (window-height . 0.25))) +(add-to-list 'display-buffer-alist + '("\\*Flymake diagnostics.*\\*" (my-display-in-bottom-panel) (window-height . 0.25))) (defun my-bottom-panel-toggle () @@ -256,8 +363,16 @@ (global-set-key (kbd "C-p") 'project-find-file) (global-set-key (kbd "C-s") 'save-buffer) (global-set-key (kbd "") 'my-select-theme) -(global-set-key (kbd "") 'xref-find-definitions) -(global-set-key (kbd "") 'xref-pop-marker-stack) +(global-set-key (kbd "C-") 'my-xref-find-definitions-at-click) +(global-set-key (kbd "C-S-") 'my-xref-find-references-at-click) +(global-set-key (kbd "") 'my-xref-find-definitions-same-pane) +(global-set-key (kbd "C-") 'xref-find-references) +(global-set-key (kbd "C-{") 'xref-go-back) +(global-set-key (kbd "C-}") 'xref-go-forward) +(global-set-key (kbd "C-]") 'stupid-indent) +(global-set-key (kbd "C-[") 'stupid-outdent) +(global-set-key (kbd "") 'xref-go-back) +(global-set-key (kbd "") 'xref-go-forward) (global-set-key (kbd "C-q") 'save-buffers-kill-terminal) (global-set-key (kbd "C-l") 'my-select-line) (global-set-key (kbd "C-e") 'my-copy-path-with-line) @@ -265,7 +380,8 @@ (define-key isearch-mode-map (kbd "") 'isearch-repeat-forward) (define-key isearch-mode-map (kbd "S-") 'isearch-repeat-backward) (define-key isearch-mode-map (kbd "") 'isearch-del-char) -(define-key isearch-mode-map (kbd "") 'isearch-exit) +(define-key isearch-mode-map (kbd "C-v") 'isearch-yank-kill) +(define-key isearch-mode-map (kbd "") 'my-isearch-cancel-or-exit) (define-key isearch-mode-map (kbd "C-g") 'my-isearch-cancel-or-exit) (defun my-isearch-cancel-or-exit () @@ -274,11 +390,69 @@ (if (string= isearch-string "") (isearch-cancel) (isearch-exit))) + +(defun my-move-line-up () + "Move current line or region up one line." + (interactive) + (if (use-region-p) + (my-move-region-up) + (let ((col (current-column))) + (transpose-lines 1) + (forward-line -2) + (move-to-column col)))) + +(defun my-move-line-down () + "Move current line or region down one line." + (interactive) + (if (use-region-p) + (my-move-region-down) + (let ((col (current-column))) + (forward-line 1) + (transpose-lines 1) + (forward-line -1) + (move-to-column col)))) + +(defun my-move-region-up () + "Move selected region up one line, keeping selection." + (let* ((rbeg (region-beginning)) + (rend (region-end)) + (beg (save-excursion (goto-char rbeg) (line-beginning-position))) + (end (save-excursion (goto-char rend) (if (bolp) (point) (1+ (line-end-position))))) + (offset-from-beg (- rbeg beg)) + (region-len (- rend rbeg)) + (text (delete-and-extract-region beg end))) + (forward-line -1) + (let ((new-beg (point))) + (insert text) + (set-mark (+ new-beg offset-from-beg)) + (goto-char (+ new-beg offset-from-beg region-len)) + (setq deactivate-mark nil)))) + +(defun my-move-region-down () + "Move selected region down one line, keeping selection." + (let* ((rbeg (region-beginning)) + (rend (region-end)) + (beg (save-excursion (goto-char rbeg) (line-beginning-position))) + (end (save-excursion (goto-char rend) (if (bolp) (point) (1+ (line-end-position))))) + (offset-from-beg (- rbeg beg)) + (region-len (- rend rbeg)) + (lines (count-lines beg end)) + (text (delete-and-extract-region beg end))) + (forward-line 1) + (let ((new-beg (point))) + (insert text) + (set-mark (+ new-beg offset-from-beg)) + (goto-char (+ new-beg offset-from-beg region-len)) + (setq deactivate-mark nil)))) + +(global-set-key (kbd "M-") 'my-move-line-up) +(global-set-key (kbd "M-") 'my-move-line-down) (setq isearch-wrap-pause 'no) ;; multiple cursors (vscode-style) (setq mc/always-run-for-all t) -(global-set-key (kbd "C-S-") 'mc/add-cursor-on-click) +(global-set-key (kbd "M-") 'ignore) +(global-set-key (kbd "M-") 'mc/add-cursor-on-click) (global-set-key (kbd "C-M-") (lambda () (interactive) (mc/mark-previous-lines 1))) (global-set-key (kbd "C-M-") (lambda () (interactive) (mc/mark-next-lines 1))) (define-key mc/keymap (kbd "") 'mc/keyboard-quit) @@ -312,6 +486,32 @@ (split-window-right) (other-window 1))))) +(defun my-xref-find-definitions-same-pane () + "Find definition and show it in the same pane. +Falls back to dumb-jump if xref fails." + (interactive) + (let ((identifier (thing-at-point 'symbol t))) + (if (null identifier) + (message "No symbol at point") + (condition-case nil + (let ((xrefs (xref-backend-definitions (xref-find-backend) identifier))) + (if xrefs + (xref-find-definitions identifier) + (dumb-jump-go))) + (error (dumb-jump-go)))))) + +(defun my-xref-find-definitions-at-click (event) + "Find definition of the symbol clicked on." + (interactive "e") + (mouse-set-point event) + (my-xref-find-definitions-same-pane)) + +(defun my-xref-find-references-at-click (event) + "Find references of the symbol clicked on." + (interactive "e") + (mouse-set-point event) + (xref-find-references (thing-at-point 'symbol t))) + ;; custom bind minor mode ;; this allows binding keys that override all other modes (defvar my-keys-minor-mode-map @@ -471,8 +671,6 @@ (visit-tags-table tags-path) (message "TAGS generated and saved: %s" tags-path))) -(global-set-key (kbd "M-") 'xref-find-references) - ;; find and replace with modes (defun my-find-replace () "Find and replace with mode selection: project, file, or selection." diff --git a/.emacs.d/lisp/simpc-mode.el b/.emacs.d/lisp/simpc-mode.el new file mode 100644 index 0000000..fc0e5f2 --- /dev/null +++ b/.emacs.d/lisp/simpc-mode.el @@ -0,0 +1,127 @@ +(require 'subr-x) + +(defvar simpc-mode-syntax-table + (let ((table (make-syntax-table))) + ;; C/C++ style comments + (modify-syntax-entry ?/ ". 124b" table) + (modify-syntax-entry ?* ". 23" table) + (modify-syntax-entry ?\n "> b" table) + ;; Preprocessor stuff? + (modify-syntax-entry ?# "." table) + ;; Chars are the same as strings + (modify-syntax-entry ?' "\"" table) + ;; Treat <> as punctuation (needed to highlight C++ keywords + ;; properly in template syntax) + (modify-syntax-entry ?< "." table) + (modify-syntax-entry ?> "." table) + + (modify-syntax-entry ?& "." table) + (modify-syntax-entry ?% "." table) + table)) + +(defun simpc-types () + '("char" "int" "long" "short" "void" "bool" "float" "double" "signed" "unsigned" + "char16_t" "char32_t" "char8_t" + "int8_t" "uint8_t" "int16_t" "uint16_t" "int32_t" "uint32_t" "int64_t" "uint64_t" + "uintptr_t" + "size_t" + "va_list")) + +(defun simpc-keywords () + '("auto" "break" "case" "const" "continue" "default" "do" + "else" "enum" "extern" "for" "goto" "if" "register" + "return" "sizeof" "static" "struct" "switch" "typedef" + "union" "volatile" "while" "alignas" "alignof" "and" + "and_eq" "asm" "atomic_cancel" "atomic_commit" "atomic_noexcept" "bitand" + "bitor" "catch" "class" "co_await" + "co_return" "co_yield" "compl" "concept" "const_cast" "consteval" "constexpr" + "constinit" "decltype" "delete" "dynamic_cast" "explicit" "export" "false" + "friend" "inline" "mutable" "namespace" "new" "noexcept" "not" "not_eq" + "nullptr" "operator" "or" "or_eq" "private" "protected" "public" "reflexpr" + "reinterpret_cast" "requires" "static_assert" "static_cast" "synchronized" + "template" "this" "thread_local" "throw" "true" "try" "typeid" "typename" + "using" "virtual" "wchar_t" "xor" "xor_eq")) + +(defun simpc-font-lock-keywords () + (list + `("# *\\(warn\\|error\\)" . font-lock-warning-face) + `("# *[#a-zA-Z0-9_]+" . font-lock-preprocessor-face) + `("# *include\\(?:_next\\)?\\s-+\\(\\(<\\|\"\\).*\\(>\\|\"\\)\\)" . (1 font-lock-string-face)) + `("\\(?:enum\\|struct\\)\\s-+\\([a-zA-Z0-9_]+\\)" . (1 font-lock-type-face)) + `(,(regexp-opt (simpc-keywords) 'symbols) . font-lock-keyword-face) + `(,(regexp-opt (simpc-types) 'symbols) . font-lock-type-face))) + +(defun simpc--previous-non-empty-line () + "Returns either NIL when there is no such line or a pair (line . indentation)" + (save-excursion + ;; If you are on the first line, but not at the beginning of buffer (BOB) the `(bobp)` + ;; function does not return `t`. So we have to move to the beginning of the line first. + ;; TODO: feel free to suggest a better approach for checking BOB here. + (move-beginning-of-line nil) + (if (bobp) + ;; If you are standing at the BOB, you by definition don't have a previous non-empty line. + nil + ;; Moving one line backwards because the current line is by definition is not + ;; the previous non-empty line. + (forward-line -1) + ;; Keep moving backwards until we hit BOB or a non-empty line. + (while (and (not (bobp)) + (string-empty-p + (string-trim-right + (thing-at-point 'line t)))) + (forward-line -1)) + + (if (string-empty-p + (string-trim-right + (thing-at-point 'line t))) + ;; If after moving backwards for this long we still look at an empty + ;; line we by definition didn't find the previous non-empty line. + nil + ;; We found the previous non-empty line! + (cons (thing-at-point 'line t) + (current-indentation)))))) + +(defun simpc--desired-indentation () + (let ((prev (simpc--previous-non-empty-line))) + (if (not prev) + (current-indentation) + (let ((indent-len 4) + (cur-line (string-trim-right (thing-at-point 'line t))) + (prev-line (string-trim-right (car prev))) + (prev-indent (cdr prev))) + (cond + ((string-match-p "^\\s-*switch\\s-*(.+)" prev-line) + prev-indent) + ((and (string-suffix-p "{" prev-line) + (string-prefix-p "}" (string-trim-left cur-line))) + prev-indent) + ((string-suffix-p "{" prev-line) + (+ prev-indent indent-len)) + ((string-prefix-p "}" (string-trim-left cur-line)) + (max (- prev-indent indent-len) 0)) + ((string-suffix-p ":" prev-line) + (if (string-suffix-p ":" cur-line) + prev-indent + (+ prev-indent indent-len))) + ((string-suffix-p ":" cur-line) + (max (- prev-indent indent-len) 0)) + (t prev-indent)))))) + +;;; TODO: customizable indentation (amount of spaces, tabs, etc) +(defun simpc-indent-line () + (interactive) + (when (not (bobp)) + (let* ((desired-indentation + (simpc--desired-indentation)) + (n (max (- (current-column) (current-indentation)) 0))) + (indent-line-to desired-indentation) + (forward-char n)))) + +(define-derived-mode simpc-mode prog-mode "Simple C" + "Simple major mode for editing C files." + :syntax-table simpc-mode-syntax-table + (setq-local font-lock-defaults '(simpc-font-lock-keywords)) + (setq-local indent-line-function 'simpc-indent-line) + (setq-local comment-start "// ")) + +(provide 'simpc-mode) diff --git a/sync.sh b/sync.sh index 467bc75..681f5db 100755 --- a/sync.sh +++ b/sync.sh @@ -84,12 +84,56 @@ copy_file() { fi } +sync_emacs_dir() { + local src_base="$1" + local dest_base="$2" + local action="$3" + + # Files to sync + local files=("custom.el" "init.el") + # Directories to sync (cleared first) + local dirs=("lisp" "themes") + + # Sync individual files + for file in "${files[@]}"; do + local src="$src_base/$file" + local dest="$dest_base/$file" + if [[ -f "$src" ]]; then + ensure_dir "$dest_base" + cp "$src" "$dest" + fi + done + + # Sync directories (clear destination first) + for dir in "${dirs[@]}"; do + local src_dir="$src_base/$dir" + local dest_dir="$dest_base/$dir" + if [[ -d "$src_dir" ]]; then + # Clear destination directory + if [[ -d "$dest_dir" ]]; then + rm -rf "$dest_dir" + fi + # Copy directory + cp -r "$src_dir" "$dest_dir" + fi + done +} + push_dotfiles() { echo "Pushing dotfiles from repo to home directory..." + # Handle .emacs.d specially (clear folders before syncing) + if [[ -d "$DOTFILES_DIR/.emacs.d" ]]; then + sync_emacs_dir "$DOTFILES_DIR/.emacs.d" "$HOME_DIR/.emacs.d" "Push" + fi + local count=0 while IFS= read -r src_file; do rel_path="${src_file#$DOTFILES_DIR/}" + # Skip .emacs.d files (handled above) + if [[ "$rel_path" == .emacs.d/* ]]; then + continue + fi dest_file="$(get_system_path "$rel_path")" copy_file "$src_file" "$dest_file" "Push" ((count++)) @@ -101,9 +145,18 @@ push_dotfiles() { pull_dotfiles() { echo "Pulling dotfiles from home directory to repo..." + # Handle .emacs.d specially (clear folders before syncing) + if [[ -d "$HOME_DIR/.emacs.d" ]]; then + sync_emacs_dir "$HOME_DIR/.emacs.d" "$DOTFILES_DIR/.emacs.d" "Pull" + fi + local count=0 while IFS= read -r dest_file; do rel_path="${dest_file#$DOTFILES_DIR/}" + # Skip .emacs.d files (handled above) + if [[ "$rel_path" == .emacs.d/* ]]; then + continue + fi src_file="$(get_system_path "$rel_path")" copy_file "$src_file" "$dest_file" "Pull" ((count++))