;;; dev-config.el --- Development tools and programming modes -*- lexical-binding: t -*-
;;; Commentary:
;; Configuration for programming languages, completion, and development tools
;;; Code:
;; Company for auto-completion (but prevent RET from completing)
(use-package company
:ensure t
:bind (:map company-active-map
("M-n" . company-select-next)
("M-p" . company-select-previous)
("<return>" . nil) ; Prevent RET from completing
("RET" . nil) ; Prevent RET from completing
("TAB" . company-complete-selection)
("<tab>" . company-complete-selection))
:custom
(company-idle-delay 0.2)
(company-minimum-prefix-length 2)
(company-show-quick-access 'left)
(company-tooltip-flip-when-above t)
(company-require-match nil) ; Don't force completion
:hook
(prog-mode . company-mode))
(use-package eglot
:ensure nil ; Built into Emacs 29+
:hook ((python-mode . eglot-ensure)
(go-mode . eglot-ensure))
:custom
(eglot-autoshutdown t)
(eldoc-echo-area-use-multiline-p 4)
(eglot-confirm-server-initiated-edits nil)
:config
(setq eglot-stay-out-of nil)
(setq eglot-report-progress nil)
(add-to-list 'eglot-server-programs
'(python-mode . ("pylsp")))
(add-to-list 'eglot-server-programs
'(go-mode . ("gopls"))))
;; breadcrumb mode for help tracking where we're at
(use-package breadcrumb
:ensure t
:init
(breadcrumb-mode 1)
:config
;; Optional: Fine-tune refresh delay
;; (setq breadcrumb-idle-delay 0.5)
;; Customize separators or max lengths if desired
;; (setq breadcrumb-imenu-crumb-separator " / ")
;; (setq breadcrumb-imenu-max-length 40)
)
(defun find-fossil-checkout-dir (&optional start-dir)
"Find the fossil checkout file.
Optional START-DIR which defaults to current."
(message "ENTERED FUNCTION")
(let* ((start-path (expand-file-name (or start-dir default-directory)))
(current-dir start-path)
found)
(message "Starting search at: %s" current-dir)
(while (and (not found)
current-dir
(not (string= current-dir "/")))
(let ((checkout-file (expand-file-name ".fslckout" current-dir)))
(message "Checking: %s" checkout-file)
(if (file-exists-p checkout-file)
(progn
(setq found current-dir)
(message "Found checkout at: %s" current-dir))
(setq current-dir
(file-name-directory (directory-file-name current-dir))))))
(or found (message "No checkout found"))))
(defun find-directory-recursive (dir match-fn &optional skip-fn)
"Recursively search DIR for a directory matching MATCH-FN.
MATCH-FN is called with the full path of each directory.
Optional SKIP-FN is called with each directory path before descending;
return t to skip that directory.
Returns the matching directory path if found, nil otherwise."
(when (and (file-directory-p dir)
(file-readable-p dir)
(not (and skip-fn (funcall skip-fn dir))))
(if (funcall match-fn dir)
dir ; Return the actual directory path when found
(let ((files (directory-files dir t directory-files-no-dot-files-regexp))
found)
(while (and files (not found))
(let ((file (car files)))
(when (file-directory-p file)
(setq found (find-directory-recursive file match-fn skip-fn)))
(setq files (cdr files))))
found))))
;; venv madness - this is kind of a mess, but it works somehow
(defun dws/set-python-path (directory)
"Set up PYTHONPATH to use the given DIRECTORY.
If eglot is running, reconnects it to pick up the new path."
(interactive "DPython path directory: ")
(let ((old-pythonpath (getenv "PYTHONPATH")))
;; Set the new PYTHONPATH
(setenv "PYTHONPATH" (expand-file-name directory))
;; Update Emacs Python paths if needed
(when (boundp 'python-shell-extra-pythonpaths)
(setq python-shell-extra-pythonpaths
(list (expand-file-name directory))))
;; Reconnect eglot if it's running and path changed
(when (and (bound-and-true-p eglot--managed-mode)
(not (string= old-pythonpath (getenv "PYTHONPATH"))))
(message "PYTHONPATH changed, reconnecting eglot...")
(eglot-reconnect))))
;; Syntax checking with Flycheck
(use-package flycheck
:diminish
:hook (after-init . global-flycheck-mode)
:custom
(flycheck-check-syntax-automatically '(save idle-changes mode-enabled))
(flycheck-idle-change-delay 1.25)
(flycheck-python-pylint-executable "~/python/bin/pylint")
(flycheck-flake8-maximum-line-length 120)
(flycheck-disabled-checkers '(go-gofmt go-build go-test rst-sphinx html-tidy)))
;; Go Mode Configuration
(use-package go-mode
:mode ("\\.go\\'" . go-mode)
:hook (before-save . dws/go-save-hook)
:config
(defun dws/go-save-hook ()
"Run formatting before saving."
(when (bound-and-true-p eglot--managed-mode)
(eglot-format-buffer)
(eglot-code-action-organize-imports nil)))
(setq indent-tabs-mode 1))
(use-package pyvenv
:ensure t
:config
(pyvenv-mode 1))
;; Python development
(use-package python
:ensure nil ; Built into Emacs
:hook ((python-mode . dws-python-mode-hook)
(python-mode . eglot-ensure))
:custom
(python-shell-interpreter (concat home-dir "/python/bin/python"))
(python-indent-guess-indent-offset nil)
(python-indent-offset 4)
(python-indent-trigger-commands '(indent-for-tab-command yas-expand yas/expand))
:config
;; Custom python-mode hook
(defun dws-python-mode-hook ()
"Simple Python setup."
(setq-local tab-width 4)
(setq-local indent-tabs-mode nil)
(setq indent-region-function 'python-indent-region)
(unbind-key "C-c c" python-mode-map)
(add-hook 'before-save-hook 'delete-trailing-whitespace nil t))
;; Handle paste and electric indent
(setq-local electric-indent-chars '(?\n)) ; Only newlines trigger reindentation
;; Add a paste advice to maintain our settings
(advice-add 'yank :after (lambda (&rest _)
(setq-local indent-tabs-mode nil)))
(advice-add 'yank-pop :after (lambda (&rest _)
(setq-local indent-tabs-mode nil)))
;; Add ignored extensions for completion
(add-to-list 'completion-ignored-extensions "pyc")
;; Clean Python style settings
(setq delete-trailing-lines t))
;; Web Development
(use-package web-mode
:mode ("\\.html\\'" . web-mode)
:hook dws/setdjangoengine
:config
(setq web-mode-engines-alist
'(("django" . "\\.dhtml\\'")
("go" . "\\.gohtml\\'")))
(setq web-mode-ac-sources-alist
'(("html" . (ac-source-emmet-html-aliases ac-source-emmet-html-snippets))
("css" . (ac-source-css-property ac-source-emmet-css-snippets)))))
;; Projectile configuration
(use-package projectile
:ensure t
:bind-keymap
("C-c p" . projectile-command-map)
:custom
(projectile-completion-system 'default) ; Works with vertico/consult
(projectile-indexing-method 'native)
(projectile-enable-caching t)
(projectile-ignored-projects '("~/" "/tmp"))
(projectile-globally-ignored-files
'(".DS_Store"))
(projectile-ignored-directories
'(".git" ".hg" "g/src/kfnm.us/vendor/" "build" ".stversions"
"sites/thesergents/public" "sites/leapfrog/public" "sites/twlk9/public"
"blog/public" "g/src/pkg" "g/bin"))
:config
;; Project type detection
(projectile-register-project-type 'fossil '(".fslckout")
:project-file ".fslckout")
(projectile-register-project-type 'python '("requirements.txt")
:project-file "requirements.txt")
(defvar my-projectile-ignored-patterns
'("\\.DS_Store$" "\\.log$" "\\.pyc$") ; Add more patterns here
"List of regex patterns to ignore in Projectile.")
(defun filter-out-ignored-files (files)
"Filter out files matching `my-projectile-ignored-patterns` from FILES."
(seq-remove (lambda (file)
(seq-some (lambda (pattern)
(string-match-p pattern file))
my-projectile-ignored-patterns))
files))
(advice-add 'projectile-project-files :filter-return #'filter-out-ignored-files)
(projectile-mode +1))
;; New version (projectile)
(defun dws/setdjangoengine ()
"Set django as web engine when in django dir."
(when (projectile-project-p)
(when (file-exists-p (expand-file-name "manage.py" (projectile-project-root)))
(web-mode-set-engine "django"))))
;; Magit Configuration
(use-package magit-mode
:config
(defun git-commit-finish-query-functions (force) t)
(setq git-commit-summary-max-length 500))
;; YASnippet Configuration
(use-package yasnippet
:diminish yas-minor-mode
:custom
(yas-snippet-dirs '("~/.emacs.d/snippets" yas-installed-snippets-dir))
:config
(yas-global-mode 1))
;; Utility Packages
(use-package avy
:demand
:bind (("M-m" . avy-goto-word-1)
("M-g f" . avy-goto-line)))
(use-package expand-region
:bind ("M-SPC" . er/expand-region))
;; Programming mode common configuration
(use-package prog-mode
:config
(defun toggle-comments ()
"Comments or uncomments region or current line if no active region."
(interactive)
(let (start end)
(if (region-active-p)
(setq start (region-beginning) end (region-end))
(setq start (line-beginning-position) end (line-end-position)))
(comment-or-uncomment-region start end)
(forward-line 1)))
:bind ("C-c c" . toggle-comments))
;; Electric pair mode configuration
(setq electric-pair-inhibit-predicate 'electric-pair-conservative-inhibit)
(electric-pair-mode t)
;; Uniquify buffer names
(use-package uniquify
:demand
:custom
(uniquify-buffer-name-style 'forward))
;; TRAMP configuration
(use-package tramp
:custom
(tramp-default-method "ssh"))
;; Ansible YAML configuration
(defun dws/in-ansible-directory-p (file)
"Check if FILE is in an ansible directory or its subdirectories."
(and file
(string-match-p "/ansible/" (file-truename file))))
(defun dws/set-yaml-mode-for-ansible-files ()
"Set yaml-mode for files without extensions in ansible directories."
(when (and (not buffer-file-name)
(dws/in-ansible-directory-p default-directory))
(yaml-mode)))
;; Add to auto-mode-alist for files without extensions in ansible directories
(add-to-list 'auto-mode-alist '("/ansible/.*/[^.]+\\'" . yaml-mode)) ; matches files without extensions
(add-hook 'find-file-hook #'dws/set-yaml-mode-for-ansible-files)
(provide 'dev-config)
;;; dev-config.el ends here