;; -*- lexical-binding: t; -*-
(defun display-python-instance ()
"Display the currently running Python instance in another window"
(interactive)
(if (eq (python-shell-get-buffer) nil)
(python-shell-get-or-create-process)
(display-buffer (python-shell-get-buffer) t)))
(defun my-python-eval-line (vis)
"Send the current line to the inferior ESS process.
Arg has same meaning as for `ess-eval-region'."
(interactive "P")
(save-excursion
(end-of-line)
(let ((end (point)))
(beginning-of-line)
(princ (concat "Loading line:...") t)
(conor-python-shell-send-region (point) end))))
(defun python-shell-send-line (&optional arg)
"This function will send the line that the point is on to the active python interpreter."
(interactive "p")
(save-excursion (move-beginning-of-line arg)
(setq start (point))
(move-end-of-line arg)
(setq end (point))
(python-shell-send-region start end)))
(defun python-shell-send-block (&optional arg)
(interactive "p")
(save-excursion (backward-paragraph)
(setq start (point))
(forward-paragraph)
(setq end (point))
(python-shell-send-region start end)))
(fset 'descend-python-dict "\M-b\M-f\C-f\C-f\['")
(defun conor-python-shell-send-string (string &optional process msg)
"Send STRING to inferior Python PROCESS.
When MSG is non-nil messages the first line of STRING."
(interactive "sPython command: ")
(let ((process (or process (python-shell-get-or-create-process)))
(lines (split-string string "\n" t)))
(when msg
(message (format "Sent: %s..." (nth 0 lines))))
(if (> (length lines) 1)
(let* ((temp-file-name (make-temp-file "py"))
(file-name (or (buffer-file-name) temp-file-name)))
(with-temp-file temp-file-name
(insert string)
(delete-trailing-whitespace))
(python-shell-send-file file-name process temp-file-name))
(comint-send-string process string)
(when (or (not (string-match "\n$" string))
(string-match "\n[ \t].*\n?$" string))
(comint-send-string process "\n")))))
(defun conor-python-shell-send-region (start end)
"Send the region delimited by START and END to inferior Python process."
(interactive "r")
(let ((deactivate-mark nil))
(conor-python-shell-send-string (buffer-substring start end) nil t)))
(defun my-add-to-multiple-hooks (func hooks)
(mapc (lambda (hook)
(add-hook hook func))
hooks))
(require 'treesit)
(add-to-list 'treesit-language-source-alist '(toml "https://github.com/tree-sitter-grammars/tree-sitter-toml"))
(unless (treesit-language-available-p 'toml)
(treesit-install-language-grammar 'toml))
(use-package uv
:straight (uv :type git :host github :repo "johannes-mueller/uv.el")
:bind ((:map python-mode-map ("C-c s" . uv))
(:map python-ts-mode-map ("C-c s" . uv))))
(defun uv-activate ()
"Activate Python environment managed by uv based on current project directory.
Looks for .venv directory in project root and activates the Python interpreter."
(interactive)
(let* ((project-root (project-root (project-current t)))
(venv-path (expand-file-name ".venv" project-root))
(python-path (expand-file-name
(if (eq system-type 'windows-nt)
"Scripts/python.exe"
"bin/python")
venv-path)))
(if (file-exists-p python-path)
(progn
;; Set Python interpreter path
(setq-local python-shell-interpreter python-path)
;; Update exec-path to include the venv's bin directory
(let ((venv-bin-dir (file-name-directory python-path)))
(setq-local exec-path (cons venv-bin-dir
(remove venv-bin-dir exec-path))))
;; Update PATH environment variable
(setenv "PATH" (concat (file-name-directory python-path)
path-separator
(getenv "PATH")))
;; Update VIRTUAL_ENV environment variable
(setenv "VIRTUAL_ENV" venv-path)
;; Remove PYTHONHOME if it exists
(setenv "PYTHONHOME" nil)
(message "Activated UV Python environment at %s" venv-path))
(error "No UV Python environment found in %s" project-root))))
(use-package python-pytest
:bind ((:map python-mode-map ("C-c t" . python-pytest-dispatch))
(:map python-ts-mode-map ("C-c t" . python-pytest-dispatch))))
(use-package py-isort)
(use-package sphinx-doc
:config
(setq sphinx-doc-include-types t))
(setq-default tab-width 4
;; Flycheck executables (kept for non-LSP fallback)
flycheck-python-pylint-executable "/Users/conornash/.local/bin/pylint"
flycheck-python-mypy-executable "/Users/conornash/.local/bin/mypy"
;; Note: flake8, pyright, ruff now handled by Eglot via ty + ruff LSP
python-indent-offset 4
python-shell-interpreter "ipython"
python-shell-interpreter-args "--simple-prompt -i --"
python-shell-prompt-detect-failure-warning t
python-shell-completion-native t
python-shell-prompt-regexp "In \\[[0-9]+\\]: "
python-shell-prompt-output-regexp "Out\\[[0-9]+\\]: "
python-shell-completion-native-disabled-interpreters '("pypy" "ipython" "jupyter")
python-shell-process-environment
'("PYTHONIOENCODING='utf-8'"
"LANG=en_US.UTF-8"
"LC_ALL=en_US.UTF-8"
"LC_LANG=en_US.UTF-8"))
(defun my-python-mode-setup ()
"Configure Python mode for development with Eglot LSP (ty + ruff)."
(rainbow-delimiters-mode 1)
(rainbow-mode 1)
(smartscan-mode 1)
(turn-on-smartparens-strict-mode)
(require 'smartparens-python)
;; Use Flymake with Eglot (not Flycheck) for LSP-based diagnostics
(flymake-mode 1)
(flycheck-mode -1)
;; Start Eglot for LSP support (ty + ruff via rass)
(eglot-ensure)
(sphinx-doc-mode 1)
(add-hook 'after-save-hook #'delete-trailing-whitespace nil t)
(guide-key/add-local-guide-key-sequence "C-c")
;; Keybindings (use local-set-key to work with both python-mode and python-ts-mode)
(local-set-key (kbd "<M-up>") 'move-text-up)
(local-set-key (kbd "<M-down>") 'move-text-down)
(local-set-key (kbd "M-\\") 'display-python-instance)
(local-set-key (kbd "C-|") 'eval-at-cursor)
(local-set-key (kbd "C-\\") 'my-python-eval-line)
(local-set-key (kbd "C-S-a") 'python-nav-beginning-of-statement)
(local-set-key (kbd "C-a") 'beginning-of-visual-line)
(local-set-key (kbd "C-S-e") 'python-nav-end-of-statement)
(local-set-key (kbd "C-e") 'end-of-visual-line)
(local-unset-key (kbd "C-c C-d"))
(local-set-key (kbd "C-c C-r") 'conor-python-shell-send-region))
;; Apply to both python-mode and python-ts-mode
(add-hook 'python-mode-hook #'my-python-mode-setup)
(add-hook 'python-ts-mode-hook #'my-python-mode-setup)