From ef79598cfcbc7a748ed3238d1dc559b2394c1ed5 Mon Sep 17 00:00:00 2001 From: Jens Luedicke Date: Fri, 5 Sep 2025 13:25:33 +0200 Subject: [PATCH] Initial Emacs configuration with modular dev mode - Main configuration in init.el - Development tools in emacs-dev-config.el (M-x enable-dev-mode) - Fixed diff-hl to use VC backend - Added Origami code folding to dev mode - Fixed Magit-delta to check for delta executable - QML files always use qml-mode in dev mode --- .gitignore | 115 +++++++ bungee.el | 587 +++++++++++++++++++++++++++++++++++ emacs-dev-config.el | 595 ++++++++++++++++++++++++++++++++++++ init-bungee.el | 63 ++++ init.el | 660 ++++++++++++++++++++++++++++++++++++++++ keybinding-reference.md | 77 +++++ qml-config.el | 113 +++++++ symbol-finder.el | 343 +++++++++++++++++++++ symbol_finder.py | 508 +++++++++++++++++++++++++++++++ 9 files changed, 3061 insertions(+) create mode 100644 .gitignore create mode 100644 bungee.el create mode 100644 emacs-dev-config.el create mode 100644 init-bungee.el create mode 100644 init.el create mode 100644 keybinding-reference.md create mode 100644 qml-config.el create mode 100644 symbol-finder.el create mode 100755 symbol_finder.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a394006 --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +# Emacs temporary and backup files +*~ +\#*\# +.\#* +*.elc +*.pyc +*.pyo + +# Auto-save files +auto-save-list/ +\#* +.saves-* + +# Backup directory +backups/ + +# Session files +.emacs.desktop +.emacs.desktop.lock +emacs-desktop +session.* +.session + +# History and state files +recentf +savehist +saveplace +places +.recentf + +# Package management +elpa/ +straight/ +quelpa/ +eln-cache/ +.cache/ + +# LSP and language server files +.lsp-session-* +.dap-breakpoints +workspace/ + +# Projectile +projectile-bookmarks.eld +projectile.cache +projects.eld + +# Treemacs +.treemacs-persist + +# Company +company-statistics-cache.el + +# Transient +transient/ + +# URL cache +url/ + +# Network security +network-security.data + +# Custom configurations (machine-specific) +custom.el +.custom.el + +# Tramp +tramp + +# Eshell +eshell/ + +# Org mode +org-clock-save.el + +# Bookmarks +bookmarks + +# Abbrev +abbrev_defs + +# Games scores +games/ + +# Calculator +calc-trail + +# Markdown preview +.markdown-preview.html + +# Symbol finder (from your config) +# symbol_finder.py - Keep this, it's needed by symbol-finder.el + +# Test files +test-*.el + +# OS-specific files +.DS_Store +Thumbs.db + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo + +# Keep these files +!init.el +!emacs-dev-config.el +!init-bungee.el +!bungee.el +!qml-config.el +!symbol-finder.el +!symbol_finder.py +!keybinding-reference.md \ No newline at end of file diff --git a/bungee.el b/bungee.el new file mode 100644 index 0000000..eca722e --- /dev/null +++ b/bungee.el @@ -0,0 +1,587 @@ +;;; bungee.el --- Fast symbol navigation with Elisp-based caching -*- lexical-binding: t; -*- + +;;; Commentary: +;; Bungee provides fast symbol navigation for C++ and QML files using +;; an Elisp-based cache system. It can work standalone or with the +;; Python symbol_finder.py for indexing. + +;;; Code: + +(require 'json) +(require 'cl-lib) +(require 'xref nil t) +(require 'pulse nil t) +(require 'grep nil t) + +(defgroup bungee nil + "Fast symbol navigation with caching." + :group 'tools) + +(defcustom bungee-cache-directory ".symbol_cache" + "Directory for symbol cache files." + :type 'string + :group 'bungee) + +(defcustom bungee-python-indexer nil + "Path to Python indexer script (optional). If nil, use Elisp indexer." + :type '(choice (const :tag "Use Elisp indexer" nil) + (file :tag "Python script path")) + :group 'bungee) + +(defcustom bungee-auto-update t + "Automatically update cache when files change." + :type 'boolean + :group 'bungee) + +(defcustom bungee-save-json-cache nil + "Also save cache in JSON format for Python compatibility." + :type 'boolean + :group 'bungee) + +;; Cache data structures +(defvar bungee--symbol-cache nil + "In-memory symbol cache. Hash table mapping file paths to symbol lists.") + +(defvar bungee--index-cache nil + "In-memory index cache. Hash table mapping file paths to modification times.") + +(defvar bungee--cache-loaded nil + "Whether the cache has been loaded from disk.") + +(cl-defstruct bungee-symbol + "A symbol in the codebase." + name ; Symbol name + file-path ; Absolute file path + line-number ; Line number (1-based) + symbol-type ; 'class, 'function, 'property, etc. + context) ; Line content for context + +;; Cache management functions +(defun bungee--cache-dir () + "Get the cache directory path." + (let ((root (or (and (fboundp 'project-current) + (project-current) + (fboundp 'project-root) + (ignore-errors + (car (project-roots (project-current))))) + default-directory))) + (expand-file-name bungee-cache-directory root))) + +(defun bungee--cache-file-path (filename) + "Get path for cache FILENAME." + (expand-file-name filename (bungee--cache-dir))) + +(defun bungee--load-cache () + "Load cache from disk into memory." + (let ((cache-file (bungee--cache-file-path "bungee-cache.el")) + (json-symbols-file (bungee--cache-file-path "symbols.json")) + (json-index-file (bungee--cache-file-path "index.json"))) + + ;; Initialize hash tables + (setq bungee--symbol-cache (make-hash-table :test 'equal)) + (setq bungee--index-cache (make-hash-table :test 'equal)) + + (cond + ;; Prefer Elisp cache if it exists + ((file-exists-p cache-file) + (load cache-file nil t) + ;; Convert loaded lists back to symbol structs + (let ((new-cache (make-hash-table :test 'equal))) + (maphash (lambda (file-path symbol-lists) + (puthash file-path + (mapcar (lambda (s) + (make-bungee-symbol + :name (nth 0 s) + :file-path (nth 1 s) + :line-number (nth 2 s) + :symbol-type (nth 3 s) + :context (nth 4 s))) + symbol-lists) + new-cache)) + bungee--symbol-cache) + (setq bungee--symbol-cache new-cache)) + (message "Loaded Elisp cache: %d files" (hash-table-count bungee--index-cache))) + + ;; Fall back to JSON if available + ((and (file-exists-p json-symbols-file) (file-exists-p json-index-file)) + (bungee--load-json-cache json-symbols-file json-index-file) + (message "Loaded JSON cache: %d files" (hash-table-count bungee--index-cache))) + + (t + (message "No cache found. Run `bungee-index-directory' to create one."))) + + (setq bungee--cache-loaded t))) + +(defun bungee--load-json-cache (symbols-file index-file) + "Load JSON cache from SYMBOLS-FILE and INDEX-FILE." + ;; Load symbols + (when (file-exists-p symbols-file) + (let* ((json-object-type 'hash-table) + (json-array-type 'list) + (json-key-type 'string) + (symbols-data (json-read-file symbols-file))) + (maphash (lambda (file-path symbols-list) + (let ((symbols (mapcar (lambda (s) + (make-bungee-symbol + :name (gethash "name" s) + :file-path (gethash "file_path" s) + :line-number (gethash "line_number" s) + :symbol-type (intern (gethash "symbol_type" s)) + :context (gethash "context" s))) + symbols-list))) + (puthash file-path symbols bungee--symbol-cache))) + symbols-data))) + + ;; Load index + (when (file-exists-p index-file) + (let* ((json-object-type 'hash-table) + (json-key-type 'string) + (index-data (json-read-file index-file))) + (maphash (lambda (file-path info) + (puthash file-path (gethash "mtime" info) bungee--index-cache)) + index-data)))) + +(defun bungee--ensure-cache () + "Ensure cache is loaded." + (unless bungee--cache-loaded + (bungee--load-cache))) + +(defun bungee--save-cache () + "Save in-memory cache to disk as Elisp code." + (let ((cache-dir (bungee--cache-dir))) + (unless (file-exists-p cache-dir) + (make-directory cache-dir t)) + + ;; Save as Elisp file for fast loading + (with-temp-file (bungee--cache-file-path "bungee-cache.el") + (insert ";;; bungee-cache.el --- Bungee symbol cache -*- lexical-binding: t; -*-\n") + (insert ";;; This file is auto-generated. Do not edit.\n\n") + + ;; Save symbol cache + (insert "(setq bungee--symbol-cache (make-hash-table :test 'equal))\n\n") + (maphash (lambda (file-path symbols) + (insert (format "(puthash %S\n '(" (abbreviate-file-name file-path))) + (dolist (symbol symbols) + (insert (format "\n %S" + (list (bungee-symbol-name symbol) + (bungee-symbol-file-path symbol) + (bungee-symbol-line-number symbol) + (bungee-symbol-symbol-type symbol) + (bungee-symbol-context symbol))))) + (insert ")\n bungee--symbol-cache)\n\n")) + bungee--symbol-cache) + + ;; Save index cache + (insert "(setq bungee--index-cache (make-hash-table :test 'equal))\n\n") + (maphash (lambda (file-path mtime) + (insert (format "(puthash %S %S bungee--index-cache)\n" + (abbreviate-file-name file-path) mtime))) + bungee--index-cache) + + (insert "\n;;; bungee-cache.el ends here\n")) + + ;; Optionally save as JSON for compatibility with Python tool + (when bungee-save-json-cache + (bungee--save-json-cache)))) + +(defun bungee--save-json-cache () + "Save cache in JSON format for Python compatibility." + (let ((cache-dir (bungee--cache-dir))) + ;; Save symbols + (let ((symbols-file (expand-file-name "symbols.json" cache-dir)) + (symbols-data (make-hash-table :test 'equal))) + (maphash (lambda (file-path symbols) + (puthash file-path + (mapcar (lambda (s) + `((name . ,(bungee-symbol-name s)) + (file_path . ,(bungee-symbol-file-path s)) + (line_number . ,(bungee-symbol-line-number s)) + (symbol_type . ,(symbol-name (bungee-symbol-symbol-type s))) + (context . ,(bungee-symbol-context s)))) + symbols) + symbols-data)) + bungee--symbol-cache) + (with-temp-file symbols-file + (insert (json-encode symbols-data)))) + + ;; Save index + (let ((index-file (expand-file-name "index.json" cache-dir)) + (index-data (make-hash-table :test 'equal))) + (maphash (lambda (file-path mtime) + (puthash file-path + `((mtime . ,mtime) + (symbol_count . ,(length (gethash file-path bungee--symbol-cache)))) + index-data)) + bungee--index-cache) + (with-temp-file index-file + (insert (json-encode index-data)))))) + +(defun bungee--file-cached-p (file-path) + "Check if FILE-PATH is cached and up to date." + (bungee--ensure-cache) + (let ((cached-mtime (gethash file-path bungee--index-cache)) + (current-mtime (and (file-exists-p file-path) + (float-time (nth 5 (file-attributes file-path)))))) + (and cached-mtime current-mtime (>= cached-mtime current-mtime)))) + +;; Elisp-based parsing +(defun bungee--parse-qml-file (file-path) + "Parse a QML file and extract symbols." + (let ((symbols '()) + (case-fold-search nil)) + (with-temp-buffer + (insert-file-contents file-path) + (goto-char (point-min)) + + ;; Find QML types + (save-excursion + (while (re-search-forward "^\\s-*\\([A-Z]\\w*\\)\\s-*{" nil t) + (push (make-bungee-symbol + :name (match-string 1) + :file-path file-path + :line-number (line-number-at-pos) + :symbol-type 'class + :context (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))) + symbols))) + + ;; Find properties (including readonly) + ;; Format: [readonly] property + (goto-char (point-min)) + (while (re-search-forward "^\\s-*\\(?:readonly\\s-+\\)?property\\s-+[\\w.<>]+\\s-+\\([a-zA-Z_]\\w*\\)" nil t) + (push (make-bungee-symbol + :name (match-string 1) + :file-path file-path + :line-number (line-number-at-pos) + :symbol-type 'property + :context (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))) + symbols)) + + ;; Find signals + (goto-char (point-min)) + (while (re-search-forward "^\\s-*signal\\s-+\\([a-zA-Z_]\\w*\\)" nil t) + (push (make-bungee-symbol + :name (match-string 1) + :file-path file-path + :line-number (line-number-at-pos) + :symbol-type 'signal + :context (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))) + symbols)) + + ;; Find functions + (goto-char (point-min)) + (while (re-search-forward "^\\s-*function\\s-+\\([a-zA-Z_]\\w*\\)\\s-*(" nil t) + (push (make-bungee-symbol + :name (match-string 1) + :file-path file-path + :line-number (line-number-at-pos) + :symbol-type 'function + :context (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))) + symbols)) + + ;; Find ids + (goto-char (point-min)) + (while (re-search-forward "^\\s-*id:\\s-*\\([a-zA-Z_]\\w*\\)" nil t) + (push (make-bungee-symbol + :name (match-string 1) + :file-path file-path + :line-number (line-number-at-pos) + :symbol-type 'variable + :context (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))) + symbols))) + + (nreverse symbols))) + +(defun bungee--parse-cpp-file (file-path) + "Parse a C++ file and extract symbols." + (let ((symbols '()) + (case-fold-search nil)) + (with-temp-buffer + (insert-file-contents file-path) + (goto-char (point-min)) + + ;; Find classes/structs + (save-excursion + (while (re-search-forward "^\\s-*\\(?:class\\|struct\\|union\\)\\s-+\\([A-Za-z_]\\w*\\)" nil t) + (push (make-bungee-symbol + :name (match-string 1) + :file-path file-path + :line-number (line-number-at-pos) + :symbol-type 'class + :context (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))) + symbols))) + + ;; Find namespaces + (goto-char (point-min)) + (while (re-search-forward "^\\s-*namespace\\s-+\\([A-Za-z_]\\w*\\)" nil t) + (push (make-bungee-symbol + :name (match-string 1) + :file-path file-path + :line-number (line-number-at-pos) + :symbol-type 'namespace + :context (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))) + symbols)) + + ;; Find enums + (goto-char (point-min)) + (while (re-search-forward "^\\s-*enum\\s-+\\(?:class\\s-+\\)?\\([A-Za-z_]\\w*\\)" nil t) + (push (make-bungee-symbol + :name (match-string 1) + :file-path file-path + :line-number (line-number-at-pos) + :symbol-type 'enum + :context (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))) + symbols)) + + ;; Basic function detection (simplified) + (goto-char (point-min)) + (while (re-search-forward "^\\s-*\\(?:\\w+\\s-+\\)*\\([A-Za-z_]\\w*\\)\\s-*([^)]*)[^;{]*{" nil t) + (let ((name (match-string 1))) + (unless (member name '("if" "while" "for" "switch" "catch")) + (push (make-bungee-symbol + :name name + :file-path file-path + :line-number (line-number-at-pos) + :symbol-type 'function + :context (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))) + symbols))))) + + (nreverse symbols))) + +(defun bungee--index-file (file-path &optional force) + "Index FILE-PATH. If FORCE is non-nil, reindex even if cached." + (bungee--ensure-cache) + (when (or force (not (bungee--file-cached-p file-path))) + (let ((symbols (cond + ((string-match-p "\\.qml\\'" file-path) + (bungee--parse-qml-file file-path)) + ((string-match-p "\\.\\(cpp\\|cc\\|cxx\\|c\\+\\+\\|hpp\\|h\\|hh\\|hxx\\|h\\+\\+\\)\\'" file-path) + (bungee--parse-cpp-file file-path)) + (t nil)))) + (when symbols + (puthash file-path symbols bungee--symbol-cache) + (puthash file-path (float-time (nth 5 (file-attributes file-path))) bungee--index-cache) + (message "Indexed %s: %d symbols" file-path (length symbols)))))) + +;; Symbol lookup functions +(defun bungee-find-symbol (symbol-name &optional exact-match) + "Find all symbols matching SYMBOL-NAME. If EXACT-MATCH is non-nil, use exact matching." + (bungee--ensure-cache) + (let ((results '())) + (maphash (lambda (_ symbols) + (dolist (symbol symbols) + (when (if exact-match + (string= (bungee-symbol-name symbol) symbol-name) + (string-match-p (regexp-quote symbol-name) + (bungee-symbol-name symbol))) + (push symbol results)))) + bungee--symbol-cache) + (sort results (lambda (a b) + (or (string< (bungee-symbol-name a) (bungee-symbol-name b)) + (< (bungee-symbol-line-number a) (bungee-symbol-line-number b))))))) + +(defun bungee-find-definition (symbol-name) + "Find the most likely definition of SYMBOL-NAME." + (let ((symbols (bungee-find-symbol symbol-name t))) + ;; Prioritize by symbol type + (or (cl-find-if (lambda (s) (eq (bungee-symbol-symbol-type s) 'class)) symbols) + (cl-find-if (lambda (s) (eq (bungee-symbol-symbol-type s) 'namespace)) symbols) + (cl-find-if (lambda (s) (eq (bungee-symbol-symbol-type s) 'function)) symbols) + (cl-find-if (lambda (s) (eq (bungee-symbol-symbol-type s) 'property)) symbols) + (car symbols)))) + +;; Interactive commands +(defun bungee-jump-to-definition () + "Jump to definition of symbol at point." + (interactive) + (let* ((symbol-name (or (thing-at-point 'symbol t) + (read-string "Symbol: "))) + (symbol (bungee-find-definition symbol-name))) + (if symbol + (progn + (push-mark) + (find-file (bungee-symbol-file-path symbol)) + (goto-char (point-min)) + (forward-line (1- (bungee-symbol-line-number symbol))) + (when (fboundp 'pulse-momentary-highlight-one-line) + (pulse-momentary-highlight-one-line (point))) + (message "Found: %s" (bungee-symbol-context symbol))) + (message "No definition found for '%s'" symbol-name)))) + +(defun bungee-find-references () + "Find all references to symbol at point." + (interactive) + (let* ((symbol-name (or (thing-at-point 'symbol t) + (read-string "Find references to: "))) + (symbols (bungee-find-symbol symbol-name))) + (if symbols + (let ((buffer (get-buffer-create "*Bungee References*"))) + (with-current-buffer buffer + (let ((inhibit-read-only t)) + (erase-buffer) + (insert (format "References to '%s':\n\n" symbol-name)) + (dolist (symbol symbols) + (insert (format "%s:%d: %s [%s]\n" + (bungee-symbol-file-path symbol) + (bungee-symbol-line-number symbol) + (bungee-symbol-context symbol) + (bungee-symbol-symbol-type symbol)))) + (goto-char (point-min)) + (grep-mode))) + (display-buffer buffer)) + (message "No references found for '%s'" symbol-name)))) + +(defun bungee-index-directory (&optional directory force) + "Index all files in DIRECTORY. If FORCE is non-nil, reindex all files." + (interactive "DDirectory to index: \nP") + (let* ((dir (or directory default-directory)) + (files (directory-files-recursively + dir + "\\.\\(qml\\|cpp\\|cc\\|cxx\\|c\\+\\+\\|hpp\\|h\\|hh\\|hxx\\|h\\+\\+\\)\\'" + nil + (lambda (d) (not (or (string-match-p "/\\." d) + (string-match-p "/node_modules" d) + (string-match-p "/CMakeFiles" d)))))) + (count 0)) + (dolist (file files) + (when (or force (not (bungee--file-cached-p file))) + (bungee--index-file file force) + (setq count (1+ count)))) + (bungee--save-cache) + (message "Indexed %d files" count))) + +(defun bungee-index-current-file () + "Index or reindex the current file." + (interactive) + (when buffer-file-name + (bungee--index-file buffer-file-name t) + (bungee--save-cache))) + +(defun bungee-cache-status () + "Show cache status." + (interactive) + (bungee--ensure-cache) + (let ((file-count (hash-table-count bungee--index-cache)) + (symbol-count 0)) + (maphash (lambda (_ symbols) + (setq symbol-count (+ symbol-count (length symbols)))) + bungee--symbol-cache) + (message "Bungee cache: %d files, %d symbols" file-count symbol-count))) + +(defun bungee-clear-cache () + "Clear the in-memory cache." + (interactive) + (setq bungee--symbol-cache nil + bungee--index-cache nil + bungee--cache-loaded nil) + (message "Bungee cache cleared")) + +;; Python indexer integration (optional) +(defun bungee-index-with-python (&optional force) + "Index using Python script if configured." + (interactive "P") + (if bungee-python-indexer + (let* ((root (or (when (fboundp 'project-root) + (car (project-roots (project-current)))) + default-directory)) + (cmd (format "python3 %s --index %s --root %s --cache-dir %s" + (shell-quote-argument bungee-python-indexer) + (if force "--force" "") + (shell-quote-argument root) + (shell-quote-argument (bungee--cache-dir))))) + (message "Running: %s" cmd) + (shell-command cmd) + (bungee-clear-cache) + (bungee--load-cache)) + (message "Python indexer not configured. Use `bungee-index-directory' instead."))) + +;; Auto-update on save +(defun bungee--after-save-hook () + "Update index after saving a file." + (when (and bungee-auto-update + buffer-file-name + (string-match-p "\\.\\(qml\\|cpp\\|cc\\|cxx\\|c\\+\\+\\|hpp\\|h\\|hh\\|hxx\\|h\\+\\+\\)\\'" + buffer-file-name)) + (bungee--index-file buffer-file-name t) + (bungee--save-cache))) + +;; Minor mode +(defvar bungee-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "M-.") 'bungee-jump-to-definition) + (define-key map (kbd "M-?") 'bungee-find-references) + (define-key map (kbd "C-c b i") 'bungee-index-directory) + (define-key map (kbd "C-c b f") 'bungee-index-current-file) + (define-key map (kbd "C-c b s") 'bungee-cache-status) + (define-key map (kbd "C-c b c") 'bungee-clear-cache) + (define-key map (kbd "C-c b p") 'bungee-index-with-python) + map) + "Keymap for bungee-mode.") + +;;;###autoload +(define-minor-mode bungee-mode + "Minor mode for fast symbol navigation with caching." + :lighter " Bungee" + :keymap bungee-mode-map + (if bungee-mode + (add-hook 'after-save-hook 'bungee--after-save-hook nil t) + (remove-hook 'after-save-hook 'bungee--after-save-hook t))) + +;;;###autoload +(define-globalized-minor-mode global-bungee-mode + bungee-mode + (lambda () + (when (and (not (minibufferp)) + buffer-file-name + (string-match-p "\\.\\(qml\\|cpp\\|cc\\|cxx\\|c\\+\\+\\|hpp\\|h\\|hh\\|hxx\\|h\\+\\+\\)\\'" + buffer-file-name)) + (bungee-mode 1)))) + +;; xref integration +(when (fboundp 'xref-make) + (defun bungee-xref-backend () 'bungee) + + (cl-defmethod xref-backend-identifier-at-point ((_backend (eql bungee))) + (thing-at-point 'symbol t)) + + (cl-defmethod xref-backend-definitions ((_backend (eql bungee)) identifier) + (let ((symbol (bungee-find-definition identifier))) + (when symbol + (list (xref-make (bungee-symbol-context symbol) + (xref-make-file-location + (bungee-symbol-file-path symbol) + (bungee-symbol-line-number symbol) + 0)))))) + + (cl-defmethod xref-backend-references ((_backend (eql bungee)) identifier) + (mapcar (lambda (symbol) + (xref-make (bungee-symbol-context symbol) + (xref-make-file-location + (bungee-symbol-file-path symbol) + (bungee-symbol-line-number symbol) + 0))) + (bungee-find-symbol identifier))) + + (add-hook 'bungee-mode-hook + (lambda () + (add-hook 'xref-backend-functions 'bungee-xref-backend nil t)))) + +(provide 'bungee) +;;; bungee.el ends here \ No newline at end of file diff --git a/emacs-dev-config.el b/emacs-dev-config.el new file mode 100644 index 0000000..88c17ad --- /dev/null +++ b/emacs-dev-config.el @@ -0,0 +1,595 @@ +;;; -*- lexical-binding: t -*- +;;; emacs-dev-config.el --- Development configuration for Emacs +;;; Commentary: +;;; Development-related configurations including LSP, languages, debugging, etc. +;;; Use M-x enable-dev-mode to activate this configuration +;;; Use M-x disable-dev-mode to deactivate this configuration + +;;; Code: + +(defvar dev-mode-enabled nil + "Flag indicating whether development mode is enabled.") + +(defvar dev-mode-packages + `(;; Development tools + lsp-mode lsp-ui lsp-treemacs + company company-box + flycheck yasnippet + projectile + ggtags multiple-cursors expand-region + hl-todo rainbow-delimiters + origami ;; Code folding + + ;; Version control + magit + ,@(when (executable-find "delta") '(magit-delta)) ;; Only if delta is installed + treemacs-magit + + ;; Helm integration for development + helm-lsp helm-xref helm-projectile + + ;; Languages + clang-format qml-mode company-qml + + ;; Debugging + dap-mode) + "List of packages required for development mode.") + +(defun dev-mode-ensure-packages () + "Ensure all development packages are installed." + (dolist (package dev-mode-packages) + (unless (package-installed-p package) + (package-refresh-contents) + (package-install package)))) + +(defun dev-mode-setup-lsp () + "Configure LSP mode for development." + (use-package lsp-mode + :ensure t + :hook ((c-mode c++-mode python-mode qml-mode) . lsp-deferred) + :commands (lsp lsp-deferred) + :config + (setq lsp-keymap-prefix "C-c l") + (setq lsp-idle-delay 0.5) + (setq lsp-log-io nil) + (setq lsp-completion-enable t) + (setq lsp-headerline-breadcrumb-enable t) + (setq lsp-enable-snippet t) + (setq lsp-enable-semantic-highlighting t)) + + (use-package lsp-ui + :ensure t + :commands lsp-ui-mode + :config + (setq lsp-ui-doc-enable t) + (setq lsp-ui-doc-position 'bottom) + (setq lsp-ui-doc-delay 1) + (setq lsp-ui-sideline-enable t) + (setq lsp-ui-sideline-show-hover t) + (setq lsp-ui-sideline-show-diagnostics t) + (setq lsp-ui-sideline-show-code-actions t)) + + (use-package helm-lsp + :ensure t + :commands helm-lsp-workspace-symbol) + + (use-package lsp-treemacs + :ensure t + :commands lsp-treemacs-errors-list)) + +(defun dev-mode-setup-company () + "Configure company mode for auto-completion." + (use-package company + :ensure t + :hook (after-init . global-company-mode) + :bind (:map company-active-map + ("C-n" . company-select-next) + ("C-p" . company-select-previous) + ("TAB" . company-complete-selection)) + :config + (setq company-idle-delay 0.2) + (setq company-minimum-prefix-length 1) + (setq company-selection-wrap-around t) + (setq company-show-numbers t) + (setq company-tooltip-align-annotations t)) + + (use-package company-box + :ensure t + :hook (company-mode . company-box-mode))) + +(defun dev-mode-setup-flycheck () + "Configure flycheck for syntax checking." + (use-package flycheck + :ensure t + :init (global-flycheck-mode) + :config + (setq flycheck-display-errors-delay 0.3) + (setq flycheck-gcc-language-standard "c++17") + (setq flycheck-clang-language-standard "c++17"))) + +(defun dev-mode-setup-yasnippet () + "Configure yasnippet for code snippets." + (use-package yasnippet + :ensure t + :config + (yas-global-mode 1))) + +(defun dev-mode-setup-projectile () + "Configure projectile for project management." + (use-package projectile + :ensure t + :init + (projectile-mode +1) + :bind-keymap ("C-c p" . projectile-command-map) + :config + (setq projectile-completion-system 'helm) + (setq projectile-switch-project-action #'projectile-dired) + (setq projectile-enable-caching t)) + + (use-package helm-projectile + :ensure t + :after (helm projectile) + :config + (helm-projectile-on))) + +(defun dev-mode-setup-ggtags () + "Configure ggtags for code navigation." + (use-package ggtags + :ensure t + :hook ((c-mode c++-mode python-mode) . ggtags-mode) + :bind (:map ggtags-mode-map + ("C-c g s" . ggtags-find-other-symbol) + ("C-c g h" . ggtags-view-tag-history) + ("C-c g r" . ggtags-find-reference) + ("C-c g f" . ggtags-find-file) + ("C-c g c" . ggtags-create-tags)) + :config + (setq ggtags-completing-read-function nil) + (setq ggtags-navigation-mode-lighter nil) + (setq ggtags-mode-line-project-name nil))) + +(defun dev-mode-setup-origami () + "Configure Origami for code folding." + (use-package origami + :ensure t + :config + ;; Define global keybindings for origami + (global-set-key (kbd "C-c f f") 'origami-toggle-node) + (global-set-key (kbd "C-c f o") 'origami-open-node) + (global-set-key (kbd "C-c f c") 'origami-close-node) + (global-set-key (kbd "C-c f a") 'origami-close-all-nodes) + (global-set-key (kbd "C-c f A") 'origami-open-all-nodes) + (global-set-key (kbd "C-c f t") 'origami-toggle-all-nodes) + (global-set-key (kbd "C-c f r") 'origami-recursively-toggle-node) + (global-set-key (kbd "C-c f R") 'origami-open-node-recursively) + (global-set-key (kbd "C-c f n") 'origami-next-fold) + (global-set-key (kbd "C-c f p") 'origami-previous-fold) + (global-set-key (kbd "C-c f s") 'origami-show-only-node) + (global-set-key (kbd "C-c f u") 'origami-undo) + (global-set-key (kbd "C-c f d") 'origami-redo) + + ;; Setup origami + (setq origami-show-fold-header t) + + ;; Enable origami mode globally for programming modes + (global-origami-mode 1) + + ;; Add hook to ensure origami works in prog-mode buffers + (add-hook 'prog-mode-hook 'origami-mode) + + ;; Initialize parsers + (origami-mode 1) + + ;; Face customization for fold markers (only if faces exist) + (when (facep 'origami-fold-header-face) + (set-face-attribute 'origami-fold-header-face nil + :box nil + :foreground "dim gray")) + (when (facep 'origami-fold-fringe-face) + (set-face-attribute 'origami-fold-fringe-face nil + :foreground "dim gray")))) + +(defun dev-mode-setup-editing-tools () + "Configure advanced editing tools." + (use-package multiple-cursors + :ensure t + :bind (("C-S-l" . mc/edit-lines) + ("C-S-d" . mc/mark-all-like-this) + ("C->" . mc/mark-next-like-this) + ("C-<" . mc/mark-previous-like-this) + ("C-c m n" . mc/skip-to-next-like-this) + ("C-c m p" . mc/skip-to-previous-like-this) + ("C-S-" . mc/add-cursor-on-click))) + + (use-package expand-region + :ensure t + :bind ("C-=" . er/expand-region)) + + (use-package hl-todo + :ensure t + :hook (prog-mode . hl-todo-mode) + :config + (setq hl-todo-keyword-faces + '(("TODO" . "#FF0000") + ("FIXME" . "#FF0000") + ("DEBUG" . "#A020F0") + ("GOTCHA" . "#FF4500") + ("STUB" . "#1E90FF")))) + + (use-package rainbow-delimiters + :ensure t + :hook (prog-mode . rainbow-delimiters-mode))) + +(defun dev-mode-setup-languages () + "Configure language-specific settings." + ;; C/C++ Configuration + (use-package cc-mode + :ensure nil + :mode (("\\.cpp\\'" . c++-mode) + ("\\.hpp\\'" . c++-mode) + ("\\.cc\\'" . c++-mode) + ("\\.hh\\'" . c++-mode) + ("\\.cxx\\'" . c++-mode) + ("\\.hxx\\'" . c++-mode)) + :config + (setq c-default-style "linux" + c-basic-offset 4) + (add-hook 'c++-mode-hook 'electric-pair-local-mode) + (add-hook 'c++-mode-hook + (lambda () + (setq compile-command + (concat "g++ -Wall -Wextra -std=c++17 -g -o " + (file-name-sans-extension (buffer-name)) + " " + (buffer-name))) + (local-set-key (kbd "C-c c") 'compile) + (local-set-key (kbd "C-c r") 'gdb) + (local-set-key (kbd "C-c C-c") 'recompile)))) + + (use-package clang-format + :ensure t + :bind (:map c++-mode-map + ("C-c C-f" . clang-format-buffer) + :map c-mode-map + ("C-c C-f" . clang-format-buffer))) + + ;; Python Configuration + (use-package python + :ensure nil + :mode ("\\.py\\'" . python-mode) + :config + (setq python-indent-offset 4) + (setq python-shell-interpreter "python3") + (add-hook 'python-mode-hook + (lambda () + (setq compile-command (concat "python3 " (buffer-name))) + (local-set-key (kbd "C-c c") 'compile) + (local-set-key (kbd "C-c r") 'run-python) + (local-set-key (kbd "C-c C-c") 'python-shell-send-buffer)))) + + ;; QML Configuration - Always use qml-mode for .qml files (not JavaScript mode) + (use-package qml-mode + :ensure t + :mode ("\\.qml\\'" . qml-mode) + :init + ;; Remove any potential js-mode associations for .qml files + (setq auto-mode-alist (delete '("\\.qml\\'" . js-mode) auto-mode-alist)) + (setq auto-mode-alist (delete '("\\.qml\\'" . javascript-mode) auto-mode-alist)) + (setq auto-mode-alist (delete '("\\.qml\\'" . js2-mode) auto-mode-alist)) + (setq auto-mode-alist (delete '("\\.qml\\'" . js3-mode) auto-mode-alist)) + (setq auto-mode-alist (delete '("\\.qml\\'" . rjsx-mode) auto-mode-alist)) + :config + ;; Ensure qml-mode is at the front of auto-mode-alist + (add-to-list 'auto-mode-alist '("\\.qml\\'" . qml-mode)) + (message "QML mode configured for .qml files")) + + (use-package company-qml + :ensure t + :after (company qml-mode) + :config + (add-to-list 'company-backends 'company-qml))) + +(defun dev-mode-setup-magit () + "Configure Magit for version control." + (use-package magit + :ensure t + :bind (("C-x g" . magit-status) + ("C-x M-g" . magit-dispatch) + ("C-c g" . magit-file-dispatch)) + :config + ;; Configure Magit integration + (setq vc-follow-symlinks t) + (setq vc-display-status t) ; Enable to show VC status in mode line + + ;; Configure Magit status sections + (setq magit-status-sections-hook + '(magit-insert-status-headers + magit-insert-merge-log + magit-insert-rebase-sequence + magit-insert-am-sequence + magit-insert-sequencer-sequence + magit-insert-bisect-output + magit-insert-bisect-rest + magit-insert-bisect-log + magit-insert-untracked-files + magit-insert-unstaged-changes + magit-insert-staged-changes + magit-insert-stashes + magit-insert-unpushed-to-pushremote + magit-insert-unpushed-to-upstream-or-recent + magit-insert-unpulled-from-pushremote + magit-insert-unpulled-from-upstream + magit-insert-recent-commits)) + + (setq magit-log-section-commit-count 10) + (setq magit-log-arguments '("--graph" "--color" "--decorate" "--all")) + (setq magit-log-section-arguments '("--graph" "--color" "--decorate" "-n256" "--all")) + (setq magit-log-margin '(t "%Y-%m-%d %H:%M " magit-log-margin-width t 18)) + (setq magit-log-show-refname-after-summary t) + (setq magit-diff-refine-hunk 'all)) + + ;; Only enable magit-delta if delta is installed + (when (executable-find "delta") + (use-package magit-delta + :ensure t + :hook (magit-mode . magit-delta-mode))) + + (use-package treemacs-magit + :ensure t + :after (treemacs magit)) + + ;; Custom Magit functions + (defun magit-save-commit-as-patch () + "Save the commit at point as a patch file." + (interactive) + (let* ((commit (or (magit-commit-at-point) + (error "No commit at point"))) + (default-name (format "%s.patch" + (substring commit 0 (min 8 (length commit))))) + (file (read-file-name "Save patch to file: " + default-directory + default-name + nil + default-name)) + (default-directory (magit-toplevel))) + (if (zerop (shell-command + (format "git format-patch -1 %s --stdout > %s" + (shell-quote-argument commit) + (shell-quote-argument (expand-file-name file))))) + (message "Patch saved to %s" file) + (error "Failed to save patch")))) + + (with-eval-after-load 'magit + (define-key magit-revision-mode-map (kbd "C-c C-p") 'magit-save-commit-as-patch) + (define-key magit-log-mode-map (kbd "C-c C-p") 'magit-save-commit-as-patch) + (define-key magit-log-select-mode-map (kbd "C-c C-p") 'magit-save-commit-as-patch)) + + ;; Optional: Integrate diff-hl with Magit when both are available + ;; Only enable if you want Magit to control diff-hl updates + ;; Comment out these lines if diff-hl has issues with Magit + (when (and (fboundp 'diff-hl-mode) + (not (bound-and-true-p diff-hl-disable-magit-integration))) + (add-hook 'magit-pre-refresh-hook 'diff-hl-magit-pre-refresh) + (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh)) + + ;; Fix common Magit issues + (defun magit-fix-refresh () + "Fix Magit refresh issues." + (interactive) + (setq magit-refresh-verbose t) + (setq magit-git-executable (executable-find "git")) + (when (and buffer-file-name (vc-backend buffer-file-name)) + (vc-refresh-state)) + (magit-refresh) + (message "Magit refresh fixed. Git executable: %s" magit-git-executable)) + + ;; Diagnose Magit issues + (defun magit-diagnose () + "Diagnose Magit configuration." + (interactive) + (let ((diagnosis '())) + (push (format "Git executable: %s" (executable-find "git")) diagnosis) + (push (format "Magit git executable: %s" magit-git-executable) diagnosis) + (push (format "Delta installed: %s" (if (executable-find "delta") "yes" "no")) diagnosis) + (push (format "Magit-delta mode: %s" (if (bound-and-true-p magit-delta-mode) "enabled" "disabled")) diagnosis) + (push (format "Default directory: %s" default-directory) diagnosis) + (push (format "In git repo: %s" (magit-toplevel)) diagnosis) + (push (format "Git version: %s" (magit-git-version)) diagnosis) + (push (format "Magit version: %s" (magit-version)) diagnosis) + (message (mapconcat 'identity (nreverse diagnosis) "\n")))) + + ;; Disable magit-delta if causing issues + (defun magit-disable-delta () + "Disable magit-delta mode." + (interactive) + (when (fboundp 'magit-delta-mode) + (magit-delta-mode -1) + (remove-hook 'magit-mode-hook 'magit-delta-mode) + (message "Magit-delta disabled. Using standard git diff.")))) + +(defun dev-mode-setup-debugging () + "Configure debugging support." + (use-package dap-mode + :ensure t + :commands dap-debug + :config + (require 'dap-python) + (require 'dap-gdb-lldb) + (dap-auto-configure-mode 1) + (setq dap-auto-configure-features '(sessions locals controls tooltip)))) + +(defun dev-mode-custom-functions () + "Define custom development functions." + (defun generate-cpp-tags () + "Generate TAGS file for C++ project." + (interactive) + (let ((project-root (or (projectile-project-root) default-directory))) + (shell-command + (format "cd %s && find . -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' -o -name '*.hh' -o -name '*.c' -o -name '*.h' | etags -" + project-root)) + (visit-tags-table (concat project-root "TAGS")) + (message "C++ TAGS file generated for project: %s" project-root))) + + (defun generate-python-tags () + "Generate TAGS file for Python project." + (interactive) + (let ((project-root (or (projectile-project-root) default-directory))) + (shell-command + (format "cd %s && find . -name '*.py' | etags -" + project-root)) + (visit-tags-table (concat project-root "TAGS")) + (message "Python TAGS file generated for project: %s" project-root))) + + (defun generate-all-tags () + "Generate TAGS file for both C++ and Python files." + (interactive) + (let ((project-root (or (projectile-project-root) default-directory))) + (shell-command + (format "cd %s && find . -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' -o -name '*.hh' -o -name '*.c' -o -name '*.h' -o -name '*.py' | etags -" + project-root)) + (visit-tags-table (concat project-root "TAGS")) + (message "TAGS file generated for all supported files in: %s" project-root))) + + (defun quick-compile-and-run () + "Quick compile and run current file." + (interactive) + (save-buffer) + (cond + ((derived-mode-p 'c++-mode) + (shell-command + (format "g++ -std=c++17 -o %s %s && ./%s" + (file-name-sans-extension (buffer-name)) + (buffer-name) + (file-name-sans-extension (buffer-name))))) + ((derived-mode-p 'python-mode) + (shell-command (format "python3 %s" (buffer-name)))) + (t (message "Unsupported file type for quick compile and run"))))) + +(defun dev-mode-setup-keybindings () + "Set up development-specific keybindings." + (global-set-key (kbd "C-c t c") 'generate-cpp-tags) + (global-set-key (kbd "C-c t p") 'generate-python-tags) + (global-set-key (kbd "C-c t a") 'generate-all-tags) + (global-set-key (kbd "C-c q") 'quick-compile-and-run) + (global-set-key (kbd "M-.") 'xref-find-definitions) + (global-set-key (kbd "M-,") 'xref-pop-marker-stack) + (global-set-key (kbd "C-M-.") 'xref-find-references) + + ;; Helper function to refresh origami in current buffer + (defun origami-reset-buffer () + "Reset origami in the current buffer." + (interactive) + (when (bound-and-true-p origami-mode) + (origami-mode -1) + (origami-mode 1) + (message "Origami reset in buffer"))) + + (global-set-key (kbd "C-c f 0") 'origami-reset-buffer)) + +(defun show-dev-mode-help () + "Show key bindings for development configuration." + (interactive) + (with-output-to-temp-buffer "*Development Mode Help*" + (princ "=== Development Mode Key Bindings ===\n\n") + (princ "LSP COMMANDS:\n") + (princ " C-c l : LSP prefix for all LSP commands\n") + (princ " M-. : Go to definition\n") + (princ " M-, : Return from definition\n") + (princ " C-M-. : Find references\n\n") + (princ "TAGS & NAVIGATION:\n") + (princ " C-c t c : Generate C++ TAGS file\n") + (princ " C-c t p : Generate Python TAGS file\n") + (princ " C-c t a : Generate TAGS for all files\n\n") + (princ "COMPILATION & EXECUTION:\n") + (princ " C-c c : Compile current file\n") + (princ " C-c r : Run (GDB for C++, Python REPL for Python)\n") + (princ " C-c q : Quick compile and run\n") + (princ " C-c C-c : Recompile (C++) or Send buffer to Python\n\n") + (princ "PROJECT MANAGEMENT:\n") + (princ " C-c p : Projectile commands prefix\n\n") + (princ "VERSION CONTROL (MAGIT):\n") + (princ " C-x g : Magit status\n") + (princ " C-x M-g : Magit dispatch\n") + (princ " C-c g : Magit file dispatch\n") + (princ " C-c C-p : Save commit as patch (in Magit buffers)\n\n") + (princ "EDITING:\n") + (princ " C-= : Expand region\n") + (princ " C-> : Mark next like this (multiple cursors)\n") + (princ " C-< : Mark previous like this\n") + (princ " C-S-d : Mark all like this\n\n") + (princ "CODE FOLDING (ORIGAMI):\n") + (princ " C-c f f : Toggle fold at point\n") + (princ " C-c f o : Open fold\n") + (princ " C-c f c : Close fold\n") + (princ " C-c f a : Close all folds\n") + (princ " C-c f A : Open all folds\n") + (princ " C-c f t : Toggle all folds\n") + (princ " C-c f r : Recursively toggle fold\n") + (princ " C-c f n/p : Next/Previous fold\n") + (princ " C-c f s : Show only current fold\n\n") + (princ "LANGUAGE MODES:\n") + (princ " .qml files : Always use qml-mode (not JavaScript mode)\n") + (princ " .py files : Python mode with LSP support\n") + (princ " .cpp/.hpp : C++ mode with LSP support\n\n") + (princ "DEVELOPMENT MODE:\n") + (princ " M-x enable-dev-mode : Enable development mode\n") + (princ " M-x disable-dev-mode : Disable development mode\n") + (princ " C-c h : Show this help\n"))) + +;;;###autoload +(defun enable-dev-mode () + "Enable development mode with all programming tools." + (interactive) + (if dev-mode-enabled + (message "Development mode is already enabled") + (message "Enabling development mode...") + ;; Ensure packages are installed + (dev-mode-ensure-packages) + ;; Set up all development features + (dev-mode-setup-lsp) + (dev-mode-setup-company) + (dev-mode-setup-flycheck) + (dev-mode-setup-yasnippet) + (dev-mode-setup-projectile) + (dev-mode-setup-ggtags) + (dev-mode-setup-origami) + (dev-mode-setup-editing-tools) + (dev-mode-setup-languages) + (dev-mode-setup-magit) + (dev-mode-setup-debugging) + (dev-mode-custom-functions) + (dev-mode-setup-keybindings) + ;; Ensure QML files use qml-mode (override any JS mode associations) + (add-to-list 'auto-mode-alist '("\\.qml\\'" . qml-mode)) + ;; Set up help command + (global-set-key (kbd "C-c h") 'show-dev-mode-help) + (setq dev-mode-enabled t) + (message "Development mode enabled! Press C-c h for help. QML files will use qml-mode."))) + +;;;###autoload +(defun disable-dev-mode () + "Disable development mode." + (interactive) + (if (not dev-mode-enabled) + (message "Development mode is not enabled") + (message "Disabling development mode...") + ;; Disable major modes that can be toggled + (global-company-mode -1) + (global-flycheck-mode -1) + (yas-global-mode -1) + (projectile-mode -1) + (global-hl-todo-mode -1) + ;; Note: Some modes might require restarting Emacs to fully disable + (setq dev-mode-enabled nil) + (message "Development mode disabled. Some features may require restarting Emacs to fully disable."))) + +;;;###autoload +(defun dev-mode-status () + "Check if development mode is enabled." + (interactive) + (if dev-mode-enabled + (message "Development mode is ENABLED") + (message "Development mode is DISABLED"))) + +(provide 'emacs-dev-config) +;;; emacs-dev-config.el ends here \ No newline at end of file diff --git a/init-bungee.el b/init-bungee.el new file mode 100644 index 0000000..2597b04 --- /dev/null +++ b/init-bungee.el @@ -0,0 +1,63 @@ +;;; init-bungee.el --- Initialize Bungee symbol finder -*- lexical-binding: t; -*- + +;; Load and configure the Bungee package +(load-file "~/.emacs.d/bungee.el") +(require 'bungee) + +;; Configure cache directory +(setq bungee-cache-directory ".symbol_cache") ; Use standard cache dir + +;; Optional: Use Python indexer for better parsing +;; Only set if the file exists +(let ((python-indexer-path "~/sources/bungee/symbol_finder.py")) + (when (file-exists-p (expand-file-name python-indexer-path)) + (setq bungee-python-indexer python-indexer-path))) + +;; Optional: Save JSON cache for Python tool compatibility +(setq bungee-save-json-cache nil) ; Set to t if you need Python compatibility + +;; Enable auto-update when saving files +(setq bungee-auto-update t) + +;; Enable Bungee mode globally for supported files +(global-bungee-mode 1) + +;; Override M-. in QML and C++ files to use Bungee +(add-hook 'qml-mode-hook + (lambda () + (local-set-key (kbd "M-.") 'bungee-jump-to-definition) + (local-set-key (kbd "M-?") 'bungee-find-references) + (local-set-key (kbd "M-,") 'pop-tag-mark))) + +(add-hook 'c++-mode-hook + (lambda () + (local-set-key (kbd "M-.") 'bungee-jump-to-definition) + (local-set-key (kbd "M-?") 'bungee-find-references) + (local-set-key (kbd "M-,") 'pop-tag-mark))) + +;; For .qml files if qml-mode is not available +(add-to-list 'auto-mode-alist '("\\.qml\\'" . js-mode)) +(add-hook 'js-mode-hook + (lambda () + (when (and buffer-file-name + (string-match-p "\\.qml\\'" buffer-file-name)) + (bungee-mode 1) + (local-set-key (kbd "M-.") 'bungee-jump-to-definition) + (local-set-key (kbd "M-?") 'bungee-find-references) + (local-set-key (kbd "M-,") 'pop-tag-mark)))) + +;; Convenient commands +(defun bungee-reindex-project () + "Reindex the entire project using Python indexer." + (interactive) + (if bungee-python-indexer + (bungee-index-with-python t) + (bungee-index-directory nil t))) + +(defun bungee-reindex-current-project () + "Force reindex current project." + (interactive) + (bungee-index-directory nil t)) + +(provide 'init-bungee) +;;; init-bungee.el ends here diff --git a/init.el b/init.el new file mode 100644 index 0000000..670b1a5 --- /dev/null +++ b/init.el @@ -0,0 +1,660 @@ +;;; -*- lexical-binding: t -*- +;;; Commentary: + +;;; Improved Emacs Configuration +;;; Keeping: treemacs, helm, diff-hl, magit + +;;; Package Management - Consolidated +(require 'package) +(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) +(package-initialize) + +;; Core package list - consolidated and cleaned +(setq package-selected-packages + '(;; Core utilities + use-package which-key + + ;; Themes + modus-themes + + ;; Version control + diff-hl + + ;; File management + treemacs treemacs-projectile treemacs-all-the-icons + neotree all-the-icons all-the-icons-dired diredfl + + ;; Helm ecosystem + helm helm-xref helm-projectile + + ;; Core project management + projectile + + ;; Markdown & Notes + markdown-mode markdown-toc grip-mode + obsidian olivetti + + ;; Search and navigation + deadgrep ripgrep wgrep anzu + ibuffer-sidebar ibuffer-projectile + + ;; Required for some functionality + org dash s f ht spinner lv hydra avy)) + +;; Auto-install missing packages +(when (cl-find-if-not #'package-installed-p package-selected-packages) + (package-refresh-contents) + (mapc #'package-install package-selected-packages)) + +;; Ensure use-package is loaded +(eval-when-compile + (require 'use-package)) +(setq use-package-always-ensure t) + +;;; Custom Settings (preserved from original) +(custom-set-variables + ;; custom-set-variables was added by Custom. + ;; If you edit it by hand, you could mess it up, so be careful. + ;; Your init file should contain only one such instance. + ;; If there is more than one, they won't work right. + '(before-save-hook '(delete-trailing-whitespace)) + '(column-number-mode t) + '(cua-mode t) + '(custom-enabled-themes '(modus-vivendi)) + '(custom-safe-themes + '("9fb69436c074b82a62b78b8d733e6274d0bd16d156f7b094e2afe4345c040c49" + "004f174754c688f358fa2afc4f8699b5db647fbfaa0d6b55ff39f63e05bfbbf5" + "ca1b398ceb1b61709197478dc7f705b8337a0a9631e399948e643520c5557382" + "75eef60308d7328ed14fa27002e85de255c2342e73275173a14ed3aa1643d545" + "77f281064ea1c8b14938866e21c4e51e4168e05db98863bd7430f1352cab294a" + "242e6f00c98aa43539b41c505ef094d21cbc981203809a82949efaa2bc6cb194" + "c9e63647d90e7dc59d52c176fbfd46fd2cfed275cd2bad9b29b6cf620d3389b6" + "ad7d874d137291e09fe2963babc33d381d087fa14928cb9d34350b67b6556b6d" + default)) + '(diff-hl-global-modes t) + '(display-line-numbers t) + '(display-line-numbers-type 'relative) + '(global-display-line-numbers-mode t) + '(ls-lisp-dirs-first t) + '(neo-show-hidden-files t) + '(neo-window-width 40) + '(package-selected-packages + '(all-the-icons all-the-icons-dired anzu avy clang-format+ commenter + cyberpunk-theme dash deadgrep diff-hl diredfl f + grip-mode helm helm-projectile helm-xref ht hydra + ibuffer-projectile ibuffer-sidebar lv magit + magit-delta markdown-mode markdown-toc modus-themes + neotree obsidian olivetti org projectile ripgrep s + spinner treemacs treemacs-all-the-icons + treemacs-magit treemacs-projectile use-package + wgrep which-key)) + '(safe-local-variable-values + '((company-backends + (company-qml company-capf company-files company-yasnippet)) + (lsp-clients-qml-server-executable . "/usr/lib/qt6/bin/qmlls") + (company-minimum-prefix-length . 1) (company-idle-delay . 0.2) + (lsp-clangd-binary-path . "clangd") + (lsp-clients-clangd-args + "--compile-commands-dir=/home/jens/sources/thulio" + "--background-index" "--clang-tidy" + "--completion-style=detailed" "--header-insertion=iwyu" + "--pch-storage=memory") + (projectile-project-root . "/home/jens/sources/thulio"))) + '(save-place-mode t) + '(show-trailing-whitespace t) + '(tool-bar-mode nil) + '(url-proxy-services + '(("https" . "eudewerepo001:3128") ("http" . "eudewerepo001:3128")))) + +(custom-set-faces + ;; custom-set-faces was added by Custom. + ;; If you edit it by hand, you could mess it up, so be careful. + ;; Your init file should contain only one such instance. + ;; If there is more than one, they won't work right. + '(default ((t (:family "0xProto Nerd Font" :foundry "0x " :slant normal :weight regular :height 120 :width normal)))) + '(diff-hl-insert ((t (:background "green3" :foreground "green3")))) + '(diff-hl-delete ((t (:background "red3" :foreground "red3")))) + '(diff-hl-change ((t (:background "blue3" :foreground "blue3"))))) + +;;; Helm Configuration (Primary interface) +(use-package helm + :ensure t + :demand t + :bind (("M-x" . helm-M-x) + ("C-x C-f" . helm-find-files) + ("C-x b" . helm-buffers-list) + ("C-x r b" . helm-bookmarks) + ("M-y" . helm-show-kill-ring) + ("C-h SPC" . helm-all-mark-rings)) + :config + (helm-mode 1) + (setq helm-split-window-in-side-p t + helm-move-to-line-cycle-in-source t + helm-ff-search-library-in-sexp t + helm-scroll-amount 8 + helm-ff-file-name-history-use-recentf t + helm-echo-input-in-header-line t) + ;; Make helm more responsive + (setq helm-input-idle-delay 0.01 + helm-cycle-resume-delay 2 + helm-follow-input-idle-delay 0.01) + ;; Prevent helm from opening new frames + (setq helm-always-two-windows nil) + (setq helm-display-buffer-default-height 15) + (setq helm-default-display-buffer-functions '(display-buffer-in-side-window))) + +(use-package helm-xref + :ensure t + :after helm) + +(use-package helm-projectile + :ensure t + :after (helm projectile) + :config + (helm-projectile-on) + ;; Prevent helm-projectile from opening new frames + (setq helm-projectile-sources-list + '(helm-source-projectile-buffers-list + helm-source-projectile-recentf-list + helm-source-projectile-files-list + helm-source-projectile-projects)) + ;; Use current window for file actions + (setq helm-display-function 'helm-default-display-buffer)) + +;;; Load development configuration (optional - use M-x enable-dev-mode) +(let ((dev-config (expand-file-name "emacs-dev-config.el" user-emacs-directory))) + (when (file-exists-p dev-config) + (load-file dev-config))) + +;;; Version Control (basic) - Magit moved to development mode + +;;; Diff-hl Configuration +(use-package diff-hl + :ensure t + :hook ((prog-mode . diff-hl-mode) + (dired-mode . diff-hl-dired-mode) + (text-mode . diff-hl-mode) + (conf-mode . diff-hl-mode)) + :bind (("M-n" . diff-hl-next-hunk) + ("M-p" . diff-hl-previous-hunk) + ("C-c v r" . diff-hl-revert-hunk) + ("C-c v s" . diff-hl-diff-goto-hunk) + ("C-c v u" . diff-hl-refresh)) + :init + ;; Set fringe width before diff-hl loads + (setq-default left-fringe-width 8) + (setq-default right-fringe-width 8) + ;; Ensure fringes are visible + (fringe-mode 8) + :config + ;; Configure VC backend for Git + (setq vc-handled-backends '(Git)) + (setq vc-git-diff-switches '("--histogram")) + + ;; IMPORTANT: Tell diff-hl to use VC backend, not Magit + (setq diff-hl-reference-revision nil) ; Use working tree, not index + (setq diff-hl-disable-on-remote nil) ; Work even on remote files + + ;; Make diff-hl use the left fringe + (setq diff-hl-side 'left) + + ;; Ensure diff-hl draws in fringe, not margin + (setq diff-hl-draw-borders nil) + (setq diff-hl-margin-mode nil) + + ;; Set diff-hl fringe bitmaps (ensure they're visible) + (setq diff-hl-fringe-bmp-function 'diff-hl-fringe-bmp-from-type) + + ;; Enable flydiff for real-time updates using VC + (diff-hl-flydiff-mode 1) + + ;; Update immediately when visiting a file + (setq diff-hl-flydiff-delay 0.3) + + ;; Make sure diff-hl updates on various events + (add-hook 'after-save-hook 'diff-hl-update) + (add-hook 'after-revert-hook 'diff-hl-update) + (add-hook 'find-file-hook 'diff-hl-update) + (add-hook 'vc-checkin-hook 'diff-hl-update) + + ;; Enable globally + (global-diff-hl-mode 1) + + ;; Explicitly disable Magit integration in base config + ;; (Magit hooks will only be added when dev-mode is enabled) + (setq diff-hl-disable-on-remote nil) + + ;; Manual refresh command + (defun diff-hl-refresh () + "Manually refresh diff-hl indicators in all buffers." + (interactive) + (dolist (buf (buffer-list)) + (with-current-buffer buf + (when diff-hl-mode + (diff-hl-update))))) + + ;; Troubleshooting function + (defun diff-hl-diagnose () + "Diagnose diff-hl issues." + (interactive) + (let ((diagnosis '())) + (push (format "diff-hl-mode: %s" (if diff-hl-mode "enabled" "disabled")) diagnosis) + (push (format "Buffer file: %s" (or buffer-file-name "none")) diagnosis) + (push (format "VC backend: %s" (or (vc-backend buffer-file-name) "none")) diagnosis) + (push (format "VC responsible backend: %s" (or (vc-responsible-backend default-directory) "none")) diagnosis) + (push (format "Modified: %s" (if (and buffer-file-name (buffer-modified-p)) "yes" "no")) diagnosis) + (push (format "VC state: %s" (when buffer-file-name (vc-state buffer-file-name))) diagnosis) + (push (format "Left fringe width: %s" left-fringe-width) diagnosis) + (push (format "Right fringe width: %s" right-fringe-width) diagnosis) + (push (format "diff-hl-side: %s" diff-hl-side) diagnosis) + (push (format "diff-hl-margin-mode: %s" diff-hl-margin-mode) diagnosis) + (push (format "diff-hl-reference-revision: %s" diff-hl-reference-revision) diagnosis) + (push (format "Magit loaded: %s" (if (fboundp 'magit-status) "yes" "no")) diagnosis) + (message (mapconcat 'identity (nreverse diagnosis) "\n")))) + + ;; Force VC to refresh its state + (defun diff-hl-force-vc-refresh () + "Force VC to refresh state and then update diff-hl." + (interactive) + (when buffer-file-name + (vc-refresh-state) + (diff-hl-update) + (message "VC state refreshed and diff-hl updated"))) + + ;; Disable Magit integration if causing issues + (defun diff-hl-disable-magit () + "Disable Magit integration with diff-hl." + (interactive) + (setq diff-hl-disable-magit-integration t) + (remove-hook 'magit-pre-refresh-hook 'diff-hl-magit-pre-refresh) + (remove-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh) + (message "Magit integration with diff-hl disabled. Using pure VC backend.")) + + ;; Ensure we're using VC backend + (defun diff-hl-ensure-vc-backend () + "Ensure diff-hl is using VC backend." + (interactive) + (setq diff-hl-reference-revision nil) + (diff-hl-disable-magit) + (diff-hl-refresh) + (message "diff-hl configured to use VC backend only"))) + +;;; Treemacs Configuration +(use-package treemacs + :ensure t + :defer t + :bind (("M-0" . treemacs-select-window) + ("C-x t t" . treemacs) + ("C-x t 1" . treemacs-delete-other-windows) + ("C-x t d" . treemacs-select-directory) + ("C-x t B" . treemacs-bookmark) + ("" . treemacs) + :map treemacs-mode-map + ("/" . treemacs-search-file) + ("C-s" . helm-projectile-find-file) + ("s" . helm-projectile-grep)) + :config + (setq treemacs-collapse-dirs (if treemacs-python-executable 3 0) + treemacs-deferred-git-apply-delay 0.5 + treemacs-directory-name-transformer #'identity + treemacs-display-in-side-window t + treemacs-eldoc-display 'simple + treemacs-file-event-delay 2000 + treemacs-file-follow-delay 0.2 + treemacs-follow-after-init t + treemacs-expand-after-init t + treemacs-find-workspace-method 'find-for-file-or-pick-first + treemacs-hide-dot-git-directory t + treemacs-indentation 2 + treemacs-indentation-string " " + treemacs-is-never-other-window nil + treemacs-max-git-entries 5000 + treemacs-missing-project-action 'ask + treemacs-move-forward-on-expand nil + treemacs-no-delete-other-windows t + treemacs-persist-file (expand-file-name ".cache/treemacs-persist" user-emacs-directory) + treemacs-position 'left + treemacs-recenter-distance 0.1 + treemacs-recenter-after-file-follow nil + treemacs-recenter-after-tag-follow nil + treemacs-recenter-after-project-jump 'always + treemacs-recenter-after-project-expand 'on-distance + treemacs-show-cursor nil + treemacs-show-hidden-files t + treemacs-silent-filewatch nil + treemacs-silent-refresh nil + treemacs-sorting 'alphabetic-asc + treemacs-space-between-root-nodes t + treemacs-tag-follow-cleanup t + treemacs-tag-follow-delay 1.5 + treemacs-width 35 + treemacs-width-is-initially-locked t + treemacs-workspace-switch-cleanup nil) + + (treemacs-follow-mode t) + (treemacs-filewatch-mode t) + (treemacs-fringe-indicator-mode 'always) + (when treemacs-python-executable + (treemacs-git-commit-diff-mode t)) + + (pcase (cons (not (null (executable-find "git"))) + (not (null treemacs-python-executable))) + (`(t . t) (treemacs-git-mode 'deferred)) + (`(t . _) (treemacs-git-mode 'simple))) + + (treemacs-hide-gitignored-files-mode nil)) + +(use-package treemacs-projectile + :ensure t + :after (treemacs projectile)) + +(use-package treemacs-all-the-icons + :ensure t + :after (treemacs all-the-icons) + :config + (treemacs-load-theme "all-the-icons")) + +;; Treemacs helper functions +(defun treemacs-toggle-and-focus () + "Toggle treemacs and focus on it if it's visible." + (interactive) + (if (treemacs-get-local-window) + (treemacs-toggle) + (progn + (treemacs) + (treemacs-select-window)))) + +(defun treemacs-search-file () + "Search for a file in the current project using helm." + (interactive) + (if (fboundp 'helm-projectile-find-file) + (helm-projectile-find-file) + (helm-find-files))) + +(defun treemacs-open-marked-files () + "Open all marked files in treemacs." + (interactive) + (when (eq major-mode 'treemacs-mode) + (treemacs-bulk-file-actions + :actions '(treemacs-visit-node-no-split)))) + +(defun treemacs-mark-visible-files () + "Mark all visible files in the current directory." + (interactive) + (when (eq major-mode 'treemacs-mode) + (save-excursion + (treemacs-goto-parent-node) + (treemacs-TAB-action) + (forward-line 1) + (while (and (not (eobp)) + (> (treemacs--get-depth-of-item) 0)) + (when (treemacs-is-node-file?) + (treemacs-do-mark)) + (forward-line 1))))) + +(global-set-key (kbd "C-c t f") 'treemacs-toggle-and-focus) +(global-set-key (kbd "C-c t s") 'treemacs-search-file) + +;;; Search Configuration (with Helm) +(use-package deadgrep + :ensure t + :bind (("C-c r" . deadgrep))) + +(use-package wgrep + :ensure t + :config + (setq wgrep-auto-save-buffer t) + (setq wgrep-enable-key "r")) + +(use-package anzu + :ensure t + :config + (global-anzu-mode 1) + :bind (([remap query-replace] . anzu-query-replace) + ([remap query-replace-regexp] . anzu-query-replace-regexp))) + +;;; Project Management +(use-package projectile + :ensure t + :init + (projectile-mode +1) + :bind-keymap ("C-c p" . projectile-command-map) + :config + (setq projectile-completion-system 'helm) + (setq projectile-switch-project-action #'projectile-dired)) + +;;; Development Tools - Moved to emacs-dev-config.el +;;; Use M-x enable-dev-mode to activate development features + +;;; Editor Enhancements +(use-package which-key + :ensure t + :init (which-key-mode) + :config + (setq which-key-idle-delay 0.3) + (setq which-key-popup-type 'side-window)) + +;;; Editor Enhancements - Development-specific features moved to emacs-dev-config.el + +(use-package all-the-icons-dired + :ensure t + :hook (dired-mode . all-the-icons-dired-mode)) + +(use-package diredfl + :ensure t + :config (diredfl-global-mode 1)) + +;; Enhanced Dired configuration for multi-file operations +(use-package dired + :ensure nil + :bind (("C-c d" . dired-jump) + ("C-c D" . projectile-dired)) + :config + (setq dired-dwim-target t) ; Guess target directory + (setq dired-recursive-copies 'always) + (setq dired-recursive-deletes 'always) + (setq dired-listing-switches "-alh --group-directories-first") + + ;; Enable multiple file marking with mouse + (define-key dired-mode-map (kbd "") 'dired-find-file) + (define-key dired-mode-map (kbd "C-") 'dired-mouse-mark) + + ;; Quick marking shortcuts + (define-key dired-mode-map (kbd "* .") 'dired-mark-extension) + (define-key dired-mode-map (kbd "* /") 'dired-mark-directories)) + + ;; Custom sorting: directories first (dotted first), then files (dotted first) + (defun dired-sort-dotfiles-first () + "Sort dired: dirs first (dots first within), then files (dots first within)." + (save-excursion + (let (buffer-read-only) + (goto-char (point-min)) + ;; Skip past the directory header + (while (and (not (eobp)) + (not (looking-at "^ \\|^\\s-*$"))) + (forward-line 1)) + (let ((start (point)) + dirs dotdirs files dotfiles special-entries) + ;; Collect all entries + (while (not (eobp)) + (when (looking-at "^ \\(.*\\)$") + (let* ((line (match-string 0)) + (filename (ignore-errors (dired-get-filename 'no-dir t)))) + (cond + ;; Keep . and .. entries separate to put at top + ((member filename '("." "..")) + (push line special-entries)) + ;; Process other entries + (filename + (let ((fullpath (ignore-errors (dired-get-filename t)))) + (when fullpath + (cond + ;; Dot directory + ((and (file-directory-p fullpath) + (string-prefix-p "." filename)) + (push line dotdirs)) + ;; Regular directory + ((file-directory-p fullpath) + (push line dirs)) + ;; Dotfile + ((string-prefix-p "." filename) + (push line dotfiles)) + ;; Regular file + (t + (push line files))))))))) + (forward-line 1)) + ;; Delete old content and insert sorted + (when (or special-entries dirs dotdirs files dotfiles) + (delete-region start (point-max)) + (goto-char start) + ;; Insert in order: . and .., dot dirs, regular dirs, dotfiles, regular files + (dolist (line (nreverse special-entries)) + (insert line "\n")) + (dolist (line (sort (nreverse dotdirs) 'string<)) + (insert line "\n")) + (dolist (line (sort (nreverse dirs) 'string<)) + (insert line "\n")) + (dolist (line (sort (nreverse dotfiles) 'string<)) + (insert line "\n")) + (dolist (line (sort (nreverse files) 'string<)) + (insert line "\n"))))) + (set-buffer-modified-p nil))) + + ;; Apply custom sorting after dired reads directory + (add-hook 'dired-after-readin-hook 'dired-sort-dotfiles-first) + +;;; Markdown Support +(use-package markdown-mode + :ensure t + :mode (("\\.md\\'" . markdown-mode) + ("\\.markdown\\'" . markdown-mode)) + :hook (markdown-mode . (lambda () + (visual-line-mode 1) + (flyspell-mode 1) + (auto-fill-mode -1))) + :bind (:map markdown-mode-map + ("C-c C-l" . markdown-insert-link) + ("C-c C-i" . markdown-insert-image) + ("C-c C-c p" . markdown-preview) + ("C-c C-c l" . markdown-live-preview-mode)) + :config + (setq markdown-command "markdown") + (setq markdown-enable-wiki-links t) + (setq markdown-italic-underscore t) + (setq markdown-asymmetric-header t) + (setq markdown-make-gfm-checkboxes-buttons t) + (setq markdown-gfm-uppercase-checkbox t) + (setq markdown-fontify-code-blocks-natively t)) + +(use-package markdown-toc + :ensure t + :after markdown-mode + :bind (:map markdown-mode-map + ("C-c C-t" . markdown-toc-generate-or-refresh-toc))) + +(use-package grip-mode + :ensure t + :after markdown-mode + :bind (:map markdown-mode-map + ("C-c C-c g" . grip-mode))) + + +;;; UI Configuration +(setq-default display-fill-column-indicator-column 80) +(setq-default display-fill-column-indicator-character ?\u2502) +(global-display-fill-column-indicator-mode 1) +(set-face-attribute 'fill-column-indicator nil :foreground "red") + +;; Enable mouse window resizing +(setq mouse-autoselect-window nil) ; Don't auto-select windows on mouse hover +(setq window-divider-default-places t) ; Show dividers everywhere +(setq window-divider-default-bottom-width 1) ; Bottom divider width +(setq window-divider-default-right-width 1) ; Right divider width +(window-divider-mode 1) ; Enable window dividers for easier mouse dragging + +;; CUA mode for rectangles +(setq cua-auto-tabify-rectangles nil) +(setq cua-keep-region-after-copy t) +(global-set-key (kbd "C-") 'cua-set-rectangle-mark) + +;;; Session Management +(desktop-save-mode 1) +(setq desktop-save t) +(setq desktop-auto-save-timeout 300) +(setq desktop-path '("~/.emacs.d/")) +(setq desktop-dirname "~/.emacs.d/") +(setq desktop-base-file-name "emacs-desktop") +(setq desktop-restore-eager 10) +(setq desktop-restore-frames t) + +(savehist-mode 1) +(setq savehist-file "~/.emacs.d/savehist") +(setq history-length 1000) + +(save-place-mode 1) +(setq save-place-file "~/.emacs.d/saveplace") + +(recentf-mode 1) +(setq recentf-max-menu-items 50) +(setq recentf-max-saved-items 200) + +;;; Performance Optimizations +(setq gc-cons-threshold 100000000) +(setq read-process-output-max (* 1024 1024)) + +;;; General Settings +(setq-default indent-tabs-mode nil) +(setq tab-width 4) +(setq inhibit-startup-screen t) +(global-auto-revert-mode t) +(electric-pair-mode 1) +(show-paren-mode 1) +(setq show-paren-delay 0) +(global-hl-line-mode 1) + +;; Use system ls for better performance and features +(setq ls-lisp-use-insert-directory-program t) ; Use system's ls command +(setq insert-directory-program "ls") ; Explicitly set to use ls + +;; Auto-save and backup settings +(setq backup-directory-alist '(("." . "~/.emacs.d/backups"))) +(setq delete-old-versions t + kept-new-versions 6 + kept-old-versions 2 + version-control t) +(setq auto-save-file-name-transforms + '((".*" "~/.emacs.d/auto-save-list/" t))) + +;;; Custom Functions +(defun kill-current-buffer-no-confirm () + "Kill the current buffer without confirmation, unless it has unsaved changes." + (interactive) + (kill-buffer (current-buffer))) + +(defun reload-emacs-config () + "Reload the Emacs configuration file." + (interactive) + (load-file (expand-file-name "init.el" user-emacs-directory)) + (message "Emacs configuration reloaded!")) + +;;; Final Keybindings +(global-set-key (kbd "C-x k") 'kill-current-buffer-no-confirm) +(global-set-key (kbd "C-c C-r") 'reload-emacs-config) +(global-set-key (kbd "C-x C-b") 'helm-buffers-list) + +;;; Development Mode Information +(defun show-dev-mode-info () + "Show information about development mode." + (interactive) + (message "Development mode is available. Use M-x enable-dev-mode to activate LSP, company-mode, flycheck, and other development tools.")) + +;; Show info on startup if dev config is loaded +(when (featurep 'emacs-dev-config) + (add-hook 'emacs-startup-hook + (lambda () + (run-with-timer 1 nil + (lambda () + (message "Development mode available. Use M-x enable-dev-mode to activate.")))))) + +(provide 'init) +;;; init.el ends here diff --git a/keybinding-reference.md b/keybinding-reference.md new file mode 100644 index 0000000..b5c0e5b --- /dev/null +++ b/keybinding-reference.md @@ -0,0 +1,77 @@ +# Emacs Keybinding Reference + +## Navigation & Interface +- `M-x` - Helm command execution +- `C-x C-f` - Helm find files +- `C-x b` - Helm switch buffer +- `C-x C-b` - Helm buffer list +- `M-y` - Helm kill ring +- `C-c h o` - Helm occur +- `C-h SPC` - Helm mark rings + +## Version Control (Magit) +- `C-x g` - Magit status +- `C-x M-g` - Magit dispatch +- `C-c g` - Magit file dispatch +- `C-c C-p` - Save commit as patch (in magit modes) + +## Diff Highlighting +- `M-n` - Next diff hunk +- `M-p` - Previous diff hunk +- `C-c v r` - Revert hunk +- `C-c v s` - Show diff at hunk +- `C-c v u` - Update diff indicators + +## Treemacs +- `M-0` - Select treemacs window +- `C-x t t` - Toggle treemacs +- `` - Toggle treemacs (alternative) +- `C-x t d` - Select directory +- `C-x t B` - Treemacs bookmark +- `C-c t f` - Toggle and focus treemacs + +## Search +- `C-c r` - Deadgrep (ripgrep search) +- `C-c p` - Projectile prefix + +## Development +- `C-c l` - LSP prefix +- `C-c c` - Compile +- `C-c q` - Quick compile and run +- `C-c t c` - Generate C++ tags +- `C-c t p` - Generate Python tags +- `C-c t a` - Generate all tags + +## Multiple Cursors +- `C-S-l` - Edit lines +- `C-S-d` - Mark all like this +- `C->` - Mark next like this +- `C-<` - Mark previous like this +- `C-c m n` - Skip to next +- `C-c m p` - Skip to previous +- `C-S-` - Add cursor on click + +## Editor +- `C-=` - Expand region +- `C-` - CUA rectangle mark + +## Markdown/Notes +- `C-c o j` - Obsidian jump to note +- `C-c o n` - New Obsidian note +- `C-c o l` - Insert Obsidian link +- `C-c o s` - Search Obsidian vault +- `C-c C-t` - Generate markdown TOC +- `C-c z` - Olivetti mode (distraction-free) + +## File Management +- `C-c n t` - Toggle neotree +- `C-c h` - Show dev config help + +## System +- `C-x k` - Kill buffer (no confirm) +- `C-c C-r` - Reload Emacs config + +## Conflicts Resolved +- Removed `C-d` override (now uses default delete-char) +- Helm replaces all Ivy/Counsel/Swiper bindings +- Single interface for completion (Helm only) \ No newline at end of file diff --git a/qml-config.el b/qml-config.el new file mode 100644 index 0000000..c09e6a9 --- /dev/null +++ b/qml-config.el @@ -0,0 +1,113 @@ +;;; -*- lexical-binding: t -*- +;; QML Language Server Configuration Helper +;; This file provides enhanced QML auto-completion setup + +;; Function to find QML language server executable +(defun find-qml-language-server () + "Find the QML language server executable in common locations." + (or (executable-find "qml-lsp") + (executable-find "qmlls") + (executable-find "qml6-lsp") + ;; Try common Qt installation paths + "/usr/lib/qt6/bin/qmlls" + "/usr/lib/x86_64-linux-gnu/qt6/bin/qmlls" + "/opt/qt6/bin/qmlls" + ;; Fallback - will show an error if not found + "qmlls")) + +;; Enhanced QML mode configuration +(use-package qml-mode + :ensure t + :mode ("\\.qml\\'" . qml-mode) + :config + ;; Set up proper indentation + (setq qml-indent-offset 4) + + ;; Add QML-specific keywords for better syntax highlighting + (font-lock-add-keywords 'qml-mode + '(("\\<\\(readonly\\|default\\|signal\\|alias\\)\\>" . font-lock-keyword-face) + ("\\<\\(Qt\\|QtQuick\\|QtQuick\\.Controls\\)\\>" . font-lock-constant-face)))) + +;; Enhanced LSP configuration for QML +(with-eval-after-load 'lsp-mode + ;; Register QML language server + (lsp-register-client + (make-lsp-client :new-connection (lsp-stdio-connection + (lambda () (list (find-qml-language-server)))) + :activation-fn (lsp-activate-on "qml") + :server-id 'qmlls)) + + ;; QML-specific LSP settings + (setq lsp-qml-server-command (find-qml-language-server)) + + ;; Enable LSP for QML files + (add-to-list 'lsp-language-id-configuration '(qml-mode . "qml"))) + +;; Enhanced company configuration for QML with error handling +(with-eval-after-load 'company + ;; Check if company-qml is working properly + (defun company-qml-safe-p () + "Check if company-qml backend is available and working." + (and (featurep 'company-qml) + (condition-case nil + (progn + (company-qml 'candidates "test") + t) + (error nil)))) + + ;; QML-specific company backends with fallback + (defun setup-qml-company-backends () + "Set up company backends for QML mode with error handling." + (if (company-qml-safe-p) + ;; Use company-qml if it works + (setq-local company-backends + '((company-qml + company-capf + company-files + company-yasnippet + company-dabbrev-code) + company-dabbrev)) + ;; Fallback without company-qml + (progn + (message "company-qml not available or has errors, using LSP-based completion") + (setq-local company-backends + '((company-capf ; LSP completions (primary for QML) + company-files ; File completions + company-yasnippet ; Snippet completions + company-dabbrev-code ; Code word completions + company-keywords) ; Language keywords + company-dabbrev))))) ; General word completions + + ;; Apply to QML mode + (add-hook 'qml-mode-hook 'setup-qml-company-backends)) + +;; QML snippets configuration +(with-eval-after-load 'yasnippet + ;; Create QML snippets directory if it doesn't exist + (let ((qml-snippets-dir (expand-file-name "snippets/qml-mode" user-emacs-directory))) + (unless (file-exists-p qml-snippets-dir) + (make-directory qml-snippets-dir t)))) + +;; Enhanced QML development settings +(add-hook 'qml-mode-hook + (lambda () + ;; Enable electric pair mode for automatic bracket/quote pairing + (electric-pair-local-mode 1) + ;; Enable automatic indentation + (electric-indent-local-mode 1) + ;; Set tab width + (setq tab-width 4) + ;; Use spaces instead of tabs + (setq indent-tabs-mode nil) + ;; Enable line numbers + (display-line-numbers-mode 1) + ;; Enable syntax checking + (flycheck-mode 1))) + +;; Key bindings for QML development +(with-eval-after-load 'qml-mode + (define-key qml-mode-map (kbd "C-c C-r") 'lsp-rename) + (define-key qml-mode-map (kbd "C-c C-d") 'lsp-find-definition) + (define-key qml-mode-map (kbd "C-c C-f") 'lsp-find-references)) + +(provide 'qml-config) diff --git a/symbol-finder.el b/symbol-finder.el new file mode 100644 index 0000000..6f19233 --- /dev/null +++ b/symbol-finder.el @@ -0,0 +1,343 @@ +;;; symbol-finder.el --- Jump to symbol definitions using Python symbol finder -*- lexical-binding: t; -*- + +;;; Commentary: +;; This package provides integration with the Python symbol_finder.py tool +;; for quickly jumping to symbol definitions in C++ and QML files. +;; +;; Usage: +;; 1. Add this file to your Emacs load path +;; 2. Add (require 'symbol-finder) to your init.el +;; 3. Optionally customize symbol-finder-python-script path +;; 4. Use M-. to jump to definition (or customize keybinding) + +;;; Code: + +(defgroup symbol-finder nil + "Jump to symbol definitions using Python symbol finder." + :group 'tools) + +(defcustom symbol-finder-python-script + (expand-file-name "symbol_finder.py" + (file-name-directory (or load-file-name buffer-file-name))) + "Path to the symbol_finder.py script." + :type 'file + :group 'symbol-finder) + +(defcustom symbol-finder-root-directory nil + "Root directory for symbol indexing. If nil, use project root or default-directory." + :type '(choice (const :tag "Auto-detect" nil) + (directory :tag "Directory")) + :group 'symbol-finder) + +(defcustom symbol-finder-cache-directory ".symbol_cache" + "Directory name for symbol cache (relative to root or absolute path)." + :type 'string + :group 'symbol-finder) + +(defcustom symbol-finder-use-absolute-cache nil + "If non-nil, treat cache-directory as an absolute path." + :type 'boolean + :group 'symbol-finder) + +(defcustom symbol-finder-auto-index t + "Whether to automatically index files when cache is missing." + :type 'boolean + :group 'symbol-finder) + +(defvar symbol-finder--history nil + "History of searched symbols.") + +(defvar symbol-finder--marker-ring (make-ring 20) + "Ring of markers for jumping back.") + +(defun symbol-finder--get-root () + "Get the root directory for symbol operations." + (or symbol-finder-root-directory + (when (fboundp 'projectile-project-root) + (projectile-project-root)) + (when (fboundp 'project-root) + (car (project-roots (project-current)))) + default-directory)) + +(defun symbol-finder--get-cache-dir () + "Get the cache directory path." + (if (or symbol-finder-use-absolute-cache + (file-name-absolute-p symbol-finder-cache-directory)) + symbol-finder-cache-directory + (expand-file-name symbol-finder-cache-directory (symbol-finder--get-root)))) + +(defun symbol-finder--run-command (args) + "Run symbol_finder.py with ARGS and return output." + (let* ((root-dir (symbol-finder--get-root)) + (cache-dir (symbol-finder--get-cache-dir)) + (default-directory root-dir) + (cmd (format "python3 %s --root %s --cache-dir %s %s" + (shell-quote-argument symbol-finder-python-script) + (shell-quote-argument root-dir) + (shell-quote-argument cache-dir) + args))) + (shell-command-to-string cmd))) + +(defun symbol-finder-index (&optional force) + "Index all source files. With prefix arg FORCE, force reindex." + (interactive "P") + (message "Indexing files...") + (let* ((args (if force "--index --force" "--index")) + (output (symbol-finder--run-command args))) + (message "%s" (string-trim output)))) + +(defun symbol-finder--parse-emacs-output (output) + "Parse Emacs-formatted output from symbol finder." + (let ((lines (split-string output "\n" t)) + results) + (dolist (line lines) + (when (string-match "\\(.+?\\):\\([0-9]+\\):\\([0-9]+\\):\\(.*\\)" line) + (push `(:file ,(match-string 1 line) + :line ,(string-to-number (match-string 2 line)) + :column ,(string-to-number (match-string 3 line)) + :context ,(match-string 4 line)) + results))) + (nreverse results))) + +(defun symbol-finder--push-mark () + "Push current position to marker ring." + (ring-insert symbol-finder--marker-ring (point-marker))) + +(defun symbol-finder-jump-to-definition (&optional symbol) + "Jump to definition of SYMBOL at point or prompt for symbol." + (interactive) + (message "symbol-finder-jump-to-definition called") ;; Debug + (let* ((symbol (or symbol + (thing-at-point 'symbol t) + (read-string "Symbol: " nil 'symbol-finder--history))) + (cmd (format "--definition %s --emacs" (shell-quote-argument symbol))) + (output (symbol-finder--run-command cmd))) + (message "Command: python3 %s %s" symbol-finder-python-script cmd) ;; Debug + (message "Output: %s" output) ;; Debug + (let ((results (symbol-finder--parse-emacs-output output))) + (cond + ((null results) + (message "No definition found for '%s'" symbol)) + ((= 1 (length results)) + (let ((result (car results))) + (symbol-finder--push-mark) + (find-file (plist-get result :file)) + (goto-char (point-min)) + (forward-line (1- (plist-get result :line))) + (move-to-column (1- (plist-get result :column))) + (pulse-momentary-highlight-one-line (point)))) + (t + (symbol-finder--select-and-jump results symbol)))))) + +(defun symbol-finder-find-references (&optional symbol) + "Find all references to SYMBOL at point or prompt for symbol." + (interactive) + (let* ((symbol (or symbol + (thing-at-point 'symbol t) + (read-string "Find references to: " nil 'symbol-finder--history))) + (output (symbol-finder--run-command + (format "--references %s --emacs" (shell-quote-argument symbol))))) + (with-current-buffer (get-buffer-create "*Symbol References*") + (let ((inhibit-read-only t)) + (erase-buffer) + (insert output) + (grep-mode) + (goto-char (point-min))) + (display-buffer (current-buffer))))) + +(defun symbol-finder-find-symbol (&optional exact) + "Find symbol by name. With prefix arg EXACT, use exact match." + (interactive "P") + (let* ((initial (thing-at-point 'symbol t)) + (symbol (read-string (format "Find symbol%s: " (if exact " (exact)" "")) + initial 'symbol-finder--history)) + (args (format "--find %s --emacs %s" + (shell-quote-argument symbol) + (if exact "--exact" ""))) + (output (symbol-finder--run-command args)) + (results (symbol-finder--parse-emacs-output output))) + (cond + ((null results) + (message "No symbols found matching '%s'" symbol)) + ((= 1 (length results)) + (let ((result (car results))) + (symbol-finder--push-mark) + (find-file (plist-get result :file)) + (goto-char (point-min)) + (forward-line (1- (plist-get result :line))) + (pulse-momentary-highlight-one-line (point)))) + (t + (symbol-finder--select-and-jump results symbol))))) + +(defun symbol-finder--select-and-jump (results symbol) + "Let user select from RESULTS and jump to selected SYMBOL." + (let* ((choices (mapcar (lambda (r) + (format "%s:%d: %s" + (file-name-nondirectory (plist-get r :file)) + (plist-get r :line) + (plist-get r :context))) + results)) + (choice (completing-read (format "Select %s: " symbol) choices nil t)) + (index (cl-position choice choices :test 'equal))) + (when index + (let ((result (nth index results))) + (symbol-finder--push-mark) + (find-file (plist-get result :file)) + (goto-char (point-min)) + (forward-line (1- (plist-get result :line))) + (pulse-momentary-highlight-one-line (point)))))) + +(defun symbol-finder-cache-status () + "Show cache status and statistics." + (interactive) + (let* ((root-dir (symbol-finder--get-root)) + (output (symbol-finder--run-command "--stats"))) + (with-current-buffer (get-buffer-create "*Symbol Cache Status*") + (let ((inhibit-read-only t)) + (erase-buffer) + (insert "Symbol Finder Cache Status\n") + (insert "==========================\n\n") + (insert (format "Root directory: %s\n" root-dir)) + (insert (format "Python script: %s\n\n" symbol-finder-python-script)) + (insert output) + (goto-char (point-min))) + (display-buffer (current-buffer))))) + +(defun symbol-finder-diagnose () + "Diagnose symbol-finder setup and keybindings." + (interactive) + (let ((global-binding (global-key-binding (kbd "M-."))) + (local-binding (local-key-binding (kbd "M-."))) + (mode-binding (and (boundp 'symbol-finder-mode-map) + (lookup-key symbol-finder-mode-map (kbd "M-."))))) + (message "=== Symbol-Finder Diagnostic ===") + (message "Symbol-finder-mode: %s" (if symbol-finder-mode "ON" "OFF")) + (message "Python script: %s" (if (file-exists-p symbol-finder-python-script) + "FOUND" "NOT FOUND")) + (message "M-. global binding: %s" global-binding) + (message "M-. local binding: %s" local-binding) + (message "M-. mode binding: %s" mode-binding) + (message "Active minor modes: %s" (mapcar 'car minor-mode-alist)) + (when (eq global-binding 'xref-find-definitions) + (message "NOTE: M-. is bound to xref. You may want to use symbol-finder-setup-override-xref")))) + +(defun symbol-finder-setup-override-xref () + "Override xref M-. binding with symbol-finder in current buffer." + (interactive) + (local-set-key (kbd "M-.") 'symbol-finder-jump-to-definition) + (local-set-key (kbd "M-,") 'symbol-finder-pop-mark) + (local-set-key (kbd "M-?") 'symbol-finder-find-references) + (message "M-. now bound to symbol-finder-jump-to-definition in this buffer")) + +(defun symbol-finder-pop-mark () + "Pop back to previous position in marker ring." + (interactive) + (if (ring-empty-p symbol-finder--marker-ring) + (message "No previous position") + (let ((marker (ring-remove symbol-finder--marker-ring 0))) + (switch-to-buffer (marker-buffer marker)) + (goto-char marker)))) + +(defun symbol-finder-update-file () + "Update index for current file." + (interactive) + (when buffer-file-name + (message "Updating index for %s..." buffer-file-name) + (let* ((args (format "--index --force --root %s" + (shell-quote-argument (file-name-directory buffer-file-name)))) + (output (symbol-finder--run-command args))) + (message "Index updated")))) + +;; Auto-update on save +(defun symbol-finder--after-save-hook () + "Hook to update index after saving." + (when (and buffer-file-name + (string-match-p "\\.\\(cpp\\|cc\\|cxx\\|c\\+\\+\\|hpp\\|h\\|hh\\|hxx\\|h\\+\\+\\|qml\\|js\\)$" + buffer-file-name)) + (symbol-finder-update-file))) + +(defcustom symbol-finder-auto-update-on-save nil + "Whether to automatically update index on file save." + :type 'boolean + :group 'symbol-finder + :set (lambda (symbol value) + (set-default symbol value) + (if value + (add-hook 'after-save-hook 'symbol-finder--after-save-hook) + (remove-hook 'after-save-hook 'symbol-finder--after-save-hook)))) + +;; Minor mode for keybindings +(defvar symbol-finder-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "M-.") 'symbol-finder-jump-to-definition) + (define-key map (kbd "M-?") 'symbol-finder-find-references) + (define-key map (kbd "M-,") 'symbol-finder-pop-mark) + (define-key map (kbd "C-c s f") 'symbol-finder-find-symbol) + (define-key map (kbd "C-c s i") 'symbol-finder-index) + (define-key map (kbd "C-c s u") 'symbol-finder-update-file) + map) + "Keymap for symbol-finder-mode.") + +;;;###autoload +(define-minor-mode symbol-finder-mode + "Minor mode for symbol navigation using Python symbol finder." + :lighter " SymF" + :keymap symbol-finder-mode-map + :group 'symbol-finder + (when symbol-finder-mode + ;; Debug message + (message "Symbol-finder-mode activated. M-. bound to: %s" + (lookup-key symbol-finder-mode-map (kbd "M-."))) + (when (and symbol-finder-auto-index + (not (file-exists-p + (expand-file-name symbol-finder-cache-directory + (symbol-finder--get-root))))) + (when (y-or-n-p "No symbol cache found. Index files now?") + (symbol-finder-index))))) + +;;;###autoload +(define-globalized-minor-mode global-symbol-finder-mode + symbol-finder-mode + (lambda () + (when (and (not (minibufferp)) + (string-match-p "\\.\\(cpp\\|cc\\|cxx\\|c\\+\\+\\|hpp\\|h\\|hh\\|hxx\\|h\\+\\+\\|qml\\|js\\)$" + (or buffer-file-name ""))) + (symbol-finder-mode 1)))) + +;; Compatibility with xref (optional) +(when (fboundp 'xref-make) + (defun symbol-finder-xref-backend () + "Symbol finder backend for xref." + 'symbol-finder) + + (cl-defmethod xref-backend-identifier-at-point ((_backend (eql symbol-finder))) + (thing-at-point 'symbol t)) + + (cl-defmethod xref-backend-definitions ((_backend (eql symbol-finder)) identifier) + (let* ((output (symbol-finder--run-command + (format "--definition %s --emacs" (shell-quote-argument identifier)))) + (results (symbol-finder--parse-emacs-output output))) + (mapcar (lambda (r) + (xref-make (plist-get r :context) + (xref-make-file-location (plist-get r :file) + (plist-get r :line) + (1- (plist-get r :column))))) + results))) + + (cl-defmethod xref-backend-references ((_backend (eql symbol-finder)) identifier) + (let* ((output (symbol-finder--run-command + (format "--references %s --emacs" (shell-quote-argument identifier)))) + (results (symbol-finder--parse-emacs-output output))) + (mapcar (lambda (r) + (xref-make (plist-get r :context) + (xref-make-file-location (plist-get r :file) + (plist-get r :line) + (1- (plist-get r :column))))) + results))) + + (add-hook 'symbol-finder-mode-hook + (lambda () + (add-hook 'xref-backend-functions 'symbol-finder-xref-backend nil t)))) + +(provide 'symbol-finder) +;;; symbol-finder.el ends here \ No newline at end of file diff --git a/symbol_finder.py b/symbol_finder.py new file mode 100755 index 0000000..4033ba0 --- /dev/null +++ b/symbol_finder.py @@ -0,0 +1,508 @@ +#!/usr/bin/env python3 +""" +Symbol finder tool for C++ and QML source files with caching support. +""" + +import os +import re +import json +import time +import hashlib +import argparse +import sys +from pathlib import Path +from typing import Dict, List, Tuple, Optional, Set +from dataclasses import dataclass, asdict +from collections import defaultdict + +@dataclass +class Symbol: + """Represents a symbol found in source code.""" + name: str + file_path: str + line_number: int + symbol_type: str # 'class', 'function', 'variable', 'property', 'signal', 'method', 'enum', 'namespace' + context: str # Line content for context + + def to_dict(self): + return asdict(self) + + @classmethod + def from_dict(cls, data): + return cls(**data) + +class SymbolCache: + """Manages caching of symbol information.""" + + def __init__(self, cache_dir: str = ".symbol_cache"): + self.cache_dir = Path(cache_dir) + self.cache_dir.mkdir(exist_ok=True) + self.index_file = self.cache_dir / "index.json" + self.symbols_file = self.cache_dir / "symbols.json" + self.index = self._load_index() + self.symbols = self._load_symbols() + + def _load_index(self) -> Dict[str, Dict]: + """Load file index from cache.""" + if self.index_file.exists(): + with open(self.index_file, 'r') as f: + return json.load(f) + return {} + + def _load_symbols(self) -> Dict[str, List[Dict]]: + """Load symbols from cache.""" + if self.symbols_file.exists(): + with open(self.symbols_file, 'r') as f: + return json.load(f) + return {} + + def save(self): + """Save cache to disk.""" + with open(self.index_file, 'w') as f: + json.dump(self.index, f, indent=2) + with open(self.symbols_file, 'w') as f: + json.dump(self.symbols, f, indent=2) + + def is_file_cached(self, file_path: str) -> bool: + """Check if file is cached and up to date.""" + if file_path not in self.index: + return False + + cached_mtime = self.index[file_path].get('mtime', 0) + current_mtime = os.path.getmtime(file_path) + return cached_mtime >= current_mtime + + def update_file(self, file_path: str, symbols: List[Symbol]): + """Update cache for a specific file.""" + self.index[file_path] = { + 'mtime': os.path.getmtime(file_path), + 'symbol_count': len(symbols) + } + self.symbols[file_path] = [s.to_dict() for s in symbols] + + def get_symbols(self, file_path: str) -> List[Symbol]: + """Get cached symbols for a file.""" + if file_path in self.symbols: + return [Symbol.from_dict(s) for s in self.symbols[file_path]] + return [] + + def get_all_symbols(self) -> List[Symbol]: + """Get all cached symbols.""" + all_symbols = [] + for file_path, symbols in self.symbols.items(): + all_symbols.extend([Symbol.from_dict(s) for s in symbols]) + return all_symbols + +class CppParser: + """Parser for C++ source files.""" + + # Patterns for C++ symbols + CLASS_PATTERN = re.compile(r'^\s*(class|struct|union)\s+([A-Za-z_]\w*)', re.MULTILINE) + FUNCTION_PATTERN = re.compile(r'^\s*(?:(?:static|inline|virtual|const|explicit|friend|extern)\s+)*(?:[\w:]+\s+)?([A-Za-z_]\w*)\s*\([^)]*\)\s*(?:const)?\s*(?:override)?\s*[{;]', re.MULTILINE) + NAMESPACE_PATTERN = re.compile(r'^\s*namespace\s+([A-Za-z_]\w*)', re.MULTILINE) + ENUM_PATTERN = re.compile(r'^\s*enum\s+(?:class\s+)?([A-Za-z_]\w*)', re.MULTILINE) + TYPEDEF_PATTERN = re.compile(r'^\s*(?:typedef|using)\s+.*\s+([A-Za-z_]\w*)\s*[;=]', re.MULTILINE) + MEMBER_VAR_PATTERN = re.compile(r'^\s*(?:(?:static|const|mutable)\s+)*(?:[\w:]+\s+)+([A-Za-z_]\w*)\s*[;=]', re.MULTILINE) + + def parse_file(self, file_path: str) -> List[Symbol]: + """Parse a C++ file and extract symbols.""" + symbols = [] + + try: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + lines = content.split('\n') + except Exception as e: + print(f"Error reading {file_path}: {e}", file=sys.stderr) + return symbols + + # Extract classes/structs + for match in self.CLASS_PATTERN.finditer(content): + line_num = content[:match.start()].count('\n') + 1 + symbols.append(Symbol( + name=match.group(2), + file_path=file_path, + line_number=line_num, + symbol_type='class', + context=lines[line_num - 1].strip() if line_num <= len(lines) else "" + )) + + # Extract namespaces + for match in self.NAMESPACE_PATTERN.finditer(content): + line_num = content[:match.start()].count('\n') + 1 + symbols.append(Symbol( + name=match.group(1), + file_path=file_path, + line_number=line_num, + symbol_type='namespace', + context=lines[line_num - 1].strip() if line_num <= len(lines) else "" + )) + + # Extract enums + for match in self.ENUM_PATTERN.finditer(content): + line_num = content[:match.start()].count('\n') + 1 + symbols.append(Symbol( + name=match.group(1), + file_path=file_path, + line_number=line_num, + symbol_type='enum', + context=lines[line_num - 1].strip() if line_num <= len(lines) else "" + )) + + # Extract functions (basic pattern, may need refinement) + for match in self.FUNCTION_PATTERN.finditer(content): + name = match.group(1) + # Filter out some common false positives + if name not in ['if', 'while', 'for', 'switch', 'return', 'delete', 'new']: + line_num = content[:match.start()].count('\n') + 1 + symbols.append(Symbol( + name=name, + file_path=file_path, + line_number=line_num, + symbol_type='function', + context=lines[line_num - 1].strip() if line_num <= len(lines) else "" + )) + + return symbols + +class QmlParser: + """Parser for QML files.""" + + # Patterns for QML symbols - more comprehensive + QML_TYPE_PATTERN = re.compile(r'^\s*([A-Z]\w*)\s*\{', re.MULTILINE) + # Match both regular and readonly properties with better type capture + PROPERTY_PATTERN = re.compile(r'^\s*(?:readonly\s+)?property\s+(?:[\w.<>]+\s+)?([a-zA-Z_]\w*)', re.MULTILINE) + SIGNAL_PATTERN = re.compile(r'^\s*signal\s+([a-zA-Z_]\w*)', re.MULTILINE) + FUNCTION_PATTERN = re.compile(r'^\s*function\s+([a-zA-Z_]\w*)\s*\(', re.MULTILINE) + ID_PATTERN = re.compile(r'^\s*id:\s*([a-zA-Z_]\w*)', re.MULTILINE) + # Match simple property assignments - but be more selective + PROPERTY_BINDING_PATTERN = re.compile(r'^\s*([a-zA-Z_]\w*):\s*(?:["\'{#]|[0-9]|true|false)', re.MULTILINE) + + def parse_file(self, file_path: str, debug: bool = False) -> List[Symbol]: + """Parse a QML file and extract symbols.""" + symbols = [] + + try: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + lines = content.split('\n') + except Exception as e: + print(f"Error reading {file_path}: {e}", file=sys.stderr) + return symbols + + if debug: + print(f"Debug: Parsing {file_path}, {len(lines)} lines", file=sys.stderr) + + # Extract QML types + for match in self.QML_TYPE_PATTERN.finditer(content): + line_num = content[:match.start()].count('\n') + 1 + symbols.append(Symbol( + name=match.group(1), + file_path=file_path, + line_number=line_num, + symbol_type='class', + context=lines[line_num - 1].strip() if line_num <= len(lines) else "" + )) + + # Extract properties (including readonly) + property_matches = list(self.PROPERTY_PATTERN.finditer(content)) + if debug: + print(f"Debug: Found {len(property_matches)} property declarations", file=sys.stderr) + + for match in property_matches: + line_num = content[:match.start()].count('\n') + 1 + name = match.group(1) + # Avoid duplicates by checking if we already have this symbol at same line + if not any(s.name == name and s.line_number == line_num for s in symbols): + symbols.append(Symbol( + name=name, + file_path=file_path, + line_number=line_num, + symbol_type='property', + context=lines[line_num - 1].strip() if line_num <= len(lines) else "" + )) + if debug and 'color' in name.lower(): + print(f"Debug: Found color property: {name} at line {line_num}", file=sys.stderr) + + # Extract property bindings (like colorMedTechNavy1: "#value") + for match in self.PROPERTY_BINDING_PATTERN.finditer(content): + line_num = content[:match.start()].count('\n') + 1 + name = match.group(1) + # Skip common keywords that aren't properties + if name not in {'import', 'if', 'else', 'for', 'while', 'return', 'var', 'let', 'const'}: + # Check if this isn't already captured + if not any(s.name == name and abs(s.line_number - line_num) < 2 for s in symbols): + symbols.append(Symbol( + name=name, + file_path=file_path, + line_number=line_num, + symbol_type='property', + context=lines[line_num - 1].strip() if line_num <= len(lines) else "" + )) + + # Extract signals + for match in self.SIGNAL_PATTERN.finditer(content): + line_num = content[:match.start()].count('\n') + 1 + symbols.append(Symbol( + name=match.group(1), + file_path=file_path, + line_number=line_num, + symbol_type='signal', + context=lines[line_num - 1].strip() if line_num <= len(lines) else "" + )) + + # Extract functions + for match in self.FUNCTION_PATTERN.finditer(content): + line_num = content[:match.start()].count('\n') + 1 + symbols.append(Symbol( + name=match.group(1), + file_path=file_path, + line_number=line_num, + symbol_type='function', + context=lines[line_num - 1].strip() if line_num <= len(lines) else "" + )) + + # Extract IDs + for match in self.ID_PATTERN.finditer(content): + line_num = content[:match.start()].count('\n') + 1 + symbols.append(Symbol( + name=match.group(1), + file_path=file_path, + line_number=line_num, + symbol_type='variable', + context=lines[line_num - 1].strip() if line_num <= len(lines) else "" + )) + + return symbols + +class SymbolFinder: + """Main symbol finder class.""" + + def __init__(self, root_dir: str = ".", cache_dir: str = ".symbol_cache"): + self.root_dir = Path(root_dir).resolve() + # Always use absolute path for cache_dir + if not Path(cache_dir).is_absolute(): + cache_dir = self.root_dir / cache_dir + self.cache = SymbolCache(str(cache_dir)) + self.cpp_parser = CppParser() + self.qml_parser = QmlParser() + self.file_extensions = { + '.cpp', '.cc', '.cxx', '.c++', '.hpp', '.h', '.hh', '.hxx', '.h++', + '.qml', '.js' + } + + def should_index_file(self, file_path: Path) -> bool: + """Check if a file should be indexed.""" + return file_path.suffix.lower() in self.file_extensions + + def index_file(self, file_path: str, debug: bool = False, force: bool = False) -> List[Symbol]: + """Index a single file.""" + file_path_obj = Path(file_path) + + if not file_path_obj.exists(): + if debug: + print(f"Debug: File does not exist: {file_path}", file=sys.stderr) + return [] + + # Check cache first (unless force is True) + if not force and self.cache.is_file_cached(file_path): + if debug: + print(f"Debug: Using cached symbols for {file_path}", file=sys.stderr) + return self.cache.get_symbols(file_path) + + # Parse file based on extension + symbols = [] + if file_path_obj.suffix.lower() in {'.qml', '.js'}: + symbols = self.qml_parser.parse_file(file_path, debug=debug) + else: + symbols = self.cpp_parser.parse_file(file_path) + + if debug: + print(f"Debug: Found {len(symbols)} symbols in {file_path}", file=sys.stderr) + + # Update cache + self.cache.update_file(file_path, symbols) + return symbols + + def index_directory(self, directory: str = None, force_reindex: bool = False, debug: bool = False): + """Index all files in a directory tree.""" + if directory is None: + directory = self.root_dir + + directory = Path(directory).resolve() + + if debug: + print(f"Debug: Indexing directory {directory}", file=sys.stderr) + + indexed_count = 0 + skipped_count = 0 + + for root, dirs, files in os.walk(directory): + # Skip hidden directories and common cache directories + # Note: 'build' removed to allow indexing build directories if needed + dirs[:] = [d for d in dirs if not d.startswith('.') and d not in {'node_modules', '__pycache__', 'CMakeFiles', 'dist', 'target'}] + + for file in files: + file_path = Path(root) / file + if self.should_index_file(file_path): + file_str = str(file_path) + if force_reindex or not self.cache.is_file_cached(file_str): + print(f"Indexing: {file_str}") + symbols = self.index_file(file_str, debug=debug, force=force_reindex) + if symbols: + indexed_count += 1 + else: + if debug: + print(f"Debug: No symbols found in {file_str}", file=sys.stderr) + else: + skipped_count += 1 + if debug: + print(f"Debug: Skipping cached file: {file_str}", file=sys.stderr) + + self.cache.save() + print(f"Indexing complete. Indexed {indexed_count} files, skipped {skipped_count} cached files.") + print(f"Total files in cache: {len(self.cache.symbols)}") + + def find_symbol(self, symbol_name: str, exact_match: bool = False) -> List[Symbol]: + """Find a symbol by name.""" + results = [] + all_symbols = self.cache.get_all_symbols() + + for symbol in all_symbols: + if exact_match: + if symbol.name == symbol_name: + results.append(symbol) + else: + if symbol_name.lower() in symbol.name.lower(): + results.append(symbol) + + # Sort by relevance (exact matches first, then by name length) + results.sort(key=lambda s: (s.name != symbol_name, len(s.name), s.name)) + return results + + def find_definition(self, symbol_name: str) -> Optional[Symbol]: + """Find the most likely definition of a symbol.""" + symbols = self.find_symbol(symbol_name, exact_match=True) + + # Prioritize classes, then functions, then others + priority_order = ['class', 'namespace', 'function', 'enum', 'property', 'signal', 'variable'] + + for symbol_type in priority_order: + for symbol in symbols: + if symbol.symbol_type == symbol_type: + return symbol + + return symbols[0] if symbols else None + + def find_references(self, symbol_name: str) -> List[Tuple[str, int, str]]: + """Find all references to a symbol (simple grep-based).""" + references = [] + + for file_path in self.cache.symbols.keys(): + try: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + lines = f.readlines() + for i, line in enumerate(lines, 1): + if re.search(r'\b' + re.escape(symbol_name) + r'\b', line): + references.append((file_path, i, line.strip())) + except Exception as e: + print(f"Error searching {file_path}: {e}", file=sys.stderr) + + return references + +def main(): + parser = argparse.ArgumentParser(description='Symbol finder for C++ and QML files') + parser.add_argument('--index', action='store_true', help='Index/reindex all files') + parser.add_argument('--force', action='store_true', help='Force reindex even if cached') + parser.add_argument('--find', metavar='SYMBOL', help='Find symbol by name') + parser.add_argument('--definition', metavar='SYMBOL', help='Find definition of symbol') + parser.add_argument('--references', metavar='SYMBOL', help='Find all references to symbol') + parser.add_argument('--exact', action='store_true', help='Exact match only') + parser.add_argument('--emacs', action='store_true', help='Output in Emacs format') + parser.add_argument('--root', metavar='DIR', default='.', help='Root directory to search') + parser.add_argument('--cache-dir', metavar='DIR', default='.symbol_cache', help='Cache directory') + parser.add_argument('--debug', action='store_true', help='Enable debug output') + parser.add_argument('--stats', action='store_true', help='Show cache statistics') + parser.add_argument('--list-files', action='store_true', help='List files that would be indexed') + + args = parser.parse_args() + + if args.debug: + print(f"Debug: Working directory: {os.getcwd()}", file=sys.stderr) + print(f"Debug: Root directory: {Path(args.root).resolve()}", file=sys.stderr) + print(f"Debug: Cache directory: {args.cache_dir}", file=sys.stderr) + + finder = SymbolFinder(args.root, args.cache_dir) + + if args.list_files: + # List all files that would be indexed + directory = Path(args.root).resolve() + print(f"Files that would be indexed from {directory}:") + count = 0 + for root, dirs, files in os.walk(directory): + dirs[:] = [d for d in dirs if not d.startswith('.') and d not in {'node_modules', '__pycache__', 'CMakeFiles', 'dist', 'target'}] + for file in files: + file_path = Path(root) / file + if finder.should_index_file(file_path): + print(f" {file_path}") + count += 1 + print(f"\nTotal: {count} files") + sys.exit(0) + + if args.stats: + print(f"Cache Statistics:") + print(f" Root: {finder.root_dir}") + print(f" Cache location: {finder.cache.cache_dir}") + print(f" Files indexed: {len(finder.cache.index)}") + total_symbols = sum(len(symbols) for symbols in finder.cache.symbols.values()) + print(f" Total symbols: {total_symbols}") + if finder.cache.index: + print(f"\nIndexed files:") + for file_path, info in list(finder.cache.index.items())[:10]: + print(f" {file_path}: {info.get('symbol_count', 0)} symbols") + if len(finder.cache.index) > 10: + print(f" ... and {len(finder.cache.index) - 10} more files") + sys.exit(0) + + if args.index: + finder.index_directory(force_reindex=args.force, debug=args.debug) + elif args.find: + symbols = finder.find_symbol(args.find, exact_match=args.exact) + if args.emacs: + # Emacs format: file:line:column: + for symbol in symbols: + print(f"{symbol.file_path}:{symbol.line_number}:1:{symbol.symbol_type} {symbol.name}") + else: + for symbol in symbols: + print(f"{symbol.name} ({symbol.symbol_type}) - {symbol.file_path}:{symbol.line_number}") + print(f" {symbol.context}") + elif args.definition: + symbol = finder.find_definition(args.definition) + if symbol: + if args.emacs: + print(f"{symbol.file_path}:{symbol.line_number}:1:") + else: + print(f"Definition: {symbol.name} ({symbol.symbol_type})") + print(f"Location: {symbol.file_path}:{symbol.line_number}") + print(f"Context: {symbol.context}") + else: + print(f"No definition found for '{args.definition}'") + sys.exit(1) + elif args.references: + refs = finder.find_references(args.references) + if args.emacs: + for file_path, line_num, context in refs: + print(f"{file_path}:{line_num}:1:{context}") + else: + for file_path, line_num, context in refs: + print(f"{file_path}:{line_num}: {context}") + else: + # Default: index if no cache exists + if not finder.cache.index: + print("No cache found. Indexing files...") + finder.index_directory() + else: + parser.print_help() + +if __name__ == '__main__': + main() \ No newline at end of file