Remove unused files
This commit is contained in:
@@ -1,451 +0,0 @@
|
|||||||
;;; portfolio-tracker-v2.el --- Portfolio tracking with transaction history -*- lexical-binding: t -*-
|
|
||||||
|
|
||||||
;;; Commentary:
|
|
||||||
;; Enhanced portfolio tracker with full transaction history and cost basis tracking
|
|
||||||
;; Supports buy/sell transactions, dividends, and multiple lots (FIFO/LIFO)
|
|
||||||
|
|
||||||
;;; Code:
|
|
||||||
|
|
||||||
(require 'cl-lib)
|
|
||||||
(require 'tabulated-list)
|
|
||||||
(require 'url)
|
|
||||||
(require 'json)
|
|
||||||
|
|
||||||
(defgroup portfolio-tracker nil
|
|
||||||
"Portfolio tracking with live price updates."
|
|
||||||
:group 'applications)
|
|
||||||
|
|
||||||
(defcustom portfolio-tracker-update-interval 300
|
|
||||||
"Interval in seconds between automatic price updates."
|
|
||||||
:type 'integer
|
|
||||||
:group 'portfolio-tracker)
|
|
||||||
|
|
||||||
(defcustom portfolio-tracker-cost-basis-method 'fifo
|
|
||||||
"Method for calculating cost basis: fifo or lifo."
|
|
||||||
:type '(choice (const :tag "FIFO" fifo)
|
|
||||||
(const :tag "LIFO" lifo))
|
|
||||||
:group 'portfolio-tracker)
|
|
||||||
|
|
||||||
;; Data structures
|
|
||||||
(cl-defstruct portfolio-transaction
|
|
||||||
date
|
|
||||||
type ; buy, sell, dividend, split
|
|
||||||
symbol
|
|
||||||
shares
|
|
||||||
price
|
|
||||||
fees
|
|
||||||
notes)
|
|
||||||
|
|
||||||
(cl-defstruct portfolio-lot
|
|
||||||
symbol
|
|
||||||
shares ; remaining shares in this lot
|
|
||||||
purchase-date
|
|
||||||
purchase-price
|
|
||||||
fees)
|
|
||||||
|
|
||||||
(cl-defstruct portfolio-holding
|
|
||||||
symbol
|
|
||||||
name
|
|
||||||
total-shares
|
|
||||||
lots ; list of portfolio-lot structs
|
|
||||||
average-cost
|
|
||||||
current-price
|
|
||||||
previous-close
|
|
||||||
value
|
|
||||||
unrealized-gain
|
|
||||||
unrealized-gain-percent
|
|
||||||
realized-gain ; from sell transactions
|
|
||||||
dividends ; total dividends received
|
|
||||||
type) ; stock, etf, crypto
|
|
||||||
|
|
||||||
(defvar portfolio-tracker-transactions nil
|
|
||||||
"List of all transactions.")
|
|
||||||
|
|
||||||
(defvar portfolio-tracker-holdings nil
|
|
||||||
"Current holdings calculated from transactions.")
|
|
||||||
|
|
||||||
(defvar portfolio-tracker-prices-cache nil
|
|
||||||
"Cache of current prices.")
|
|
||||||
|
|
||||||
;; Transaction processing
|
|
||||||
(defun portfolio-tracker-process-transactions ()
|
|
||||||
"Process all transactions to calculate current holdings."
|
|
||||||
(let ((holdings-table (make-hash-table :test 'equal))
|
|
||||||
(realized-gains (make-hash-table :test 'equal))
|
|
||||||
(dividends-table (make-hash-table :test 'equal)))
|
|
||||||
|
|
||||||
;; Sort transactions by date
|
|
||||||
(setq portfolio-tracker-transactions
|
|
||||||
(sort portfolio-tracker-transactions
|
|
||||||
(lambda (a b)
|
|
||||||
(string< (portfolio-transaction-date a)
|
|
||||||
(portfolio-transaction-date b)))))
|
|
||||||
|
|
||||||
;; Process each transaction
|
|
||||||
(dolist (txn portfolio-tracker-transactions)
|
|
||||||
(let ((symbol (portfolio-transaction-symbol txn))
|
|
||||||
(type (portfolio-transaction-type txn)))
|
|
||||||
|
|
||||||
(cond
|
|
||||||
;; Buy transaction - add lot
|
|
||||||
((eq type 'buy)
|
|
||||||
(let* ((holding (gethash symbol holdings-table))
|
|
||||||
(new-lot (make-portfolio-lot
|
|
||||||
:symbol symbol
|
|
||||||
:shares (portfolio-transaction-shares txn)
|
|
||||||
:purchase-date (portfolio-transaction-date txn)
|
|
||||||
:purchase-price (portfolio-transaction-price txn)
|
|
||||||
:fees (or (portfolio-transaction-fees txn) 0))))
|
|
||||||
(if holding
|
|
||||||
(push new-lot (portfolio-holding-lots holding))
|
|
||||||
(puthash symbol
|
|
||||||
(make-portfolio-holding
|
|
||||||
:symbol symbol
|
|
||||||
:lots (list new-lot)
|
|
||||||
:realized-gain 0
|
|
||||||
:dividends 0)
|
|
||||||
holdings-table))))
|
|
||||||
|
|
||||||
;; Sell transaction - remove shares using FIFO/LIFO
|
|
||||||
((eq type 'sell)
|
|
||||||
(let ((holding (gethash symbol holdings-table))
|
|
||||||
(shares-to-sell (portfolio-transaction-shares txn))
|
|
||||||
(sell-price (portfolio-transaction-price txn))
|
|
||||||
(realized 0))
|
|
||||||
(when holding
|
|
||||||
(let ((lots (if (eq portfolio-tracker-cost-basis-method 'lifo)
|
|
||||||
(reverse (portfolio-holding-lots holding))
|
|
||||||
(portfolio-holding-lots holding))))
|
|
||||||
(dolist (lot lots)
|
|
||||||
(when (> shares-to-sell 0)
|
|
||||||
(let ((shares-from-lot (min shares-to-sell
|
|
||||||
(portfolio-lot-shares lot))))
|
|
||||||
(setq realized
|
|
||||||
(+ realized
|
|
||||||
(* shares-from-lot
|
|
||||||
(- sell-price (portfolio-lot-purchase-price lot)))))
|
|
||||||
(setf (portfolio-lot-shares lot)
|
|
||||||
(- (portfolio-lot-shares lot) shares-from-lot))
|
|
||||||
(setq shares-to-sell (- shares-to-sell shares-from-lot)))))
|
|
||||||
;; Remove empty lots
|
|
||||||
(setf (portfolio-holding-lots holding)
|
|
||||||
(cl-remove-if (lambda (lot) (<= (portfolio-lot-shares lot) 0))
|
|
||||||
(portfolio-holding-lots holding)))
|
|
||||||
;; Add to realized gains
|
|
||||||
(setf (portfolio-holding-realized-gain holding)
|
|
||||||
(+ (portfolio-holding-realized-gain holding) realized))))))
|
|
||||||
|
|
||||||
;; Dividend transaction
|
|
||||||
((eq type 'dividend)
|
|
||||||
(let ((holding (gethash symbol holdings-table)))
|
|
||||||
(when holding
|
|
||||||
(setf (portfolio-holding-dividends holding)
|
|
||||||
(+ (portfolio-holding-dividends holding)
|
|
||||||
(* (portfolio-transaction-shares txn)
|
|
||||||
(portfolio-transaction-price txn)))))))))
|
|
||||||
|
|
||||||
;; Calculate totals for each holding
|
|
||||||
(maphash (lambda (symbol holding)
|
|
||||||
(let ((total-shares 0)
|
|
||||||
(total-cost 0))
|
|
||||||
(dolist (lot (portfolio-holding-lots holding))
|
|
||||||
(setq total-shares (+ total-shares (portfolio-lot-shares lot)))
|
|
||||||
(setq total-cost (+ total-cost
|
|
||||||
(* (portfolio-lot-shares lot)
|
|
||||||
(portfolio-lot-purchase-price lot)))))
|
|
||||||
(setf (portfolio-holding-total-shares holding) total-shares)
|
|
||||||
(setf (portfolio-holding-average-cost holding)
|
|
||||||
(if (> total-shares 0)
|
|
||||||
(/ total-cost total-shares)
|
|
||||||
0))))
|
|
||||||
holdings-table)
|
|
||||||
|
|
||||||
;; Convert hash table to list
|
|
||||||
(setq portfolio-tracker-holdings nil)
|
|
||||||
(maphash (lambda (symbol holding)
|
|
||||||
(when (> (portfolio-holding-total-shares holding) 0)
|
|
||||||
(push holding portfolio-tracker-holdings)))
|
|
||||||
holdings-table)))
|
|
||||||
|
|
||||||
;; Yahoo Finance integration
|
|
||||||
(defun portfolio-tracker-fetch-price (symbol callback)
|
|
||||||
"Fetch current price for SYMBOL and call CALLBACK with the result."
|
|
||||||
(let ((url (format "https://query1.finance.yahoo.com/v8/finance/chart/%s" symbol)))
|
|
||||||
(url-retrieve url
|
|
||||||
(lambda (status)
|
|
||||||
(if (plist-get status :error)
|
|
||||||
(message "Error fetching price for %s" symbol)
|
|
||||||
(goto-char (point-min))
|
|
||||||
(re-search-forward "^$")
|
|
||||||
(let* ((json-object-type 'plist)
|
|
||||||
(json-array-type 'list)
|
|
||||||
(json (json-read))
|
|
||||||
(result (plist-get json :chart))
|
|
||||||
(data (car (plist-get result :result)))
|
|
||||||
(meta (plist-get data :meta))
|
|
||||||
(price (plist-get meta :regularMarketPrice))
|
|
||||||
(prev-close (plist-get meta :previousClose))
|
|
||||||
(name (plist-get meta :longName)))
|
|
||||||
(funcall callback symbol price prev-close name))))
|
|
||||||
nil t)))
|
|
||||||
|
|
||||||
(defun portfolio-tracker-update-prices ()
|
|
||||||
"Update prices for all holdings."
|
|
||||||
(interactive)
|
|
||||||
(message "Updating prices...")
|
|
||||||
(dolist (holding portfolio-tracker-holdings)
|
|
||||||
(portfolio-tracker-fetch-price
|
|
||||||
(portfolio-holding-symbol holding)
|
|
||||||
(lambda (symbol price prev-close name)
|
|
||||||
(let ((h (cl-find symbol portfolio-tracker-holdings
|
|
||||||
:key #'portfolio-holding-symbol
|
|
||||||
:test #'string=)))
|
|
||||||
(when h
|
|
||||||
(setf (portfolio-holding-current-price h) price)
|
|
||||||
(setf (portfolio-holding-previous-close h) prev-close)
|
|
||||||
(when name
|
|
||||||
(setf (portfolio-holding-name h) name))
|
|
||||||
;; Calculate unrealized gains
|
|
||||||
(let ((total-cost 0))
|
|
||||||
(dolist (lot (portfolio-holding-lots h))
|
|
||||||
(setq total-cost (+ total-cost
|
|
||||||
(* (portfolio-lot-shares lot)
|
|
||||||
(portfolio-lot-purchase-price lot)))))
|
|
||||||
(setf (portfolio-holding-value h)
|
|
||||||
(* (portfolio-holding-total-shares h) price))
|
|
||||||
(setf (portfolio-holding-unrealized-gain h)
|
|
||||||
(- (portfolio-holding-value h) total-cost))
|
|
||||||
(setf (portfolio-holding-unrealized-gain-percent h)
|
|
||||||
(if (> total-cost 0)
|
|
||||||
(* 100 (/ (portfolio-holding-unrealized-gain h) total-cost))
|
|
||||||
0))))
|
|
||||||
(portfolio-tracker-refresh-display))))))
|
|
||||||
|
|
||||||
;; Display functions
|
|
||||||
(eval-and-compile
|
|
||||||
(defvar portfolio-tracker-mode-map
|
|
||||||
(let ((map (make-sparse-keymap)))
|
|
||||||
;; Use string keys directly for single characters
|
|
||||||
(define-key map "g" 'portfolio-tracker-refresh)
|
|
||||||
(define-key map "a" 'portfolio-tracker-add-transaction)
|
|
||||||
(define-key map "t" 'portfolio-tracker-show-transactions)
|
|
||||||
(define-key map "h" 'portfolio-tracker-refresh-display)
|
|
||||||
(define-key map "s" 'portfolio-tracker-save)
|
|
||||||
(define-key map "l" 'portfolio-tracker-load)
|
|
||||||
(define-key map "r" 'portfolio-tracker-refresh)
|
|
||||||
(define-key map "q" 'quit-window)
|
|
||||||
map)
|
|
||||||
"Keymap for portfolio-tracker-mode.")
|
|
||||||
|
|
||||||
(define-derived-mode portfolio-tracker-mode tabulated-list-mode "Portfolio"
|
|
||||||
"Major mode for tracking investment portfolio.
|
|
||||||
\\{portfolio-tracker-mode-map}"
|
|
||||||
:keymap portfolio-tracker-mode-map
|
|
||||||
;; Disable CUA mode to allow single-key commands
|
|
||||||
(setq-local cua-mode nil)
|
|
||||||
(setq-local cua-enable-cua-keys nil)
|
|
||||||
(setq tabulated-list-format
|
|
||||||
[("Symbol" 8 t)
|
|
||||||
("Name" 20 t)
|
|
||||||
("Shares" 10 t)
|
|
||||||
("Avg Cost" 10 nil)
|
|
||||||
("Current" 10 nil)
|
|
||||||
("Value" 12 nil)
|
|
||||||
("Unrealized" 12 nil)
|
|
||||||
("Realized" 10 nil)
|
|
||||||
("Dividends" 10 nil)
|
|
||||||
("Total %" 8 nil)])
|
|
||||||
(setq tabulated-list-padding 2)
|
|
||||||
(setq tabulated-list-sort-key (cons "Symbol" nil))
|
|
||||||
(tabulated-list-init-header)))
|
|
||||||
|
|
||||||
(define-derived-mode portfolio-transactions-mode tabulated-list-mode "Transactions"
|
|
||||||
"Major mode for viewing portfolio transactions."
|
|
||||||
(setq tabulated-list-format
|
|
||||||
[("Date" 12 t)
|
|
||||||
("Type" 8 t)
|
|
||||||
("Symbol" 8 t)
|
|
||||||
("Shares" 10 nil)
|
|
||||||
("Price" 10 nil)
|
|
||||||
("Total" 12 nil)
|
|
||||||
("Fees" 8 nil)
|
|
||||||
("Notes" 30 nil)])
|
|
||||||
(setq tabulated-list-padding 2)
|
|
||||||
(setq tabulated-list-sort-key (cons "Date" t))
|
|
||||||
(tabulated-list-init-header))
|
|
||||||
|
|
||||||
(defun portfolio-tracker-refresh-display ()
|
|
||||||
"Refresh the holdings display."
|
|
||||||
(interactive)
|
|
||||||
(let ((buf (get-buffer "*Portfolio Holdings*")))
|
|
||||||
(when buf
|
|
||||||
(with-current-buffer buf
|
|
||||||
(let ((inhibit-read-only t))
|
|
||||||
(setq tabulated-list-entries
|
|
||||||
(mapcar (lambda (holding)
|
|
||||||
(list holding
|
|
||||||
(vector
|
|
||||||
(portfolio-holding-symbol holding)
|
|
||||||
(or (portfolio-holding-name holding) "")
|
|
||||||
(format "%.2f" (portfolio-holding-total-shares holding))
|
|
||||||
(format "$%.2f" (portfolio-holding-average-cost holding))
|
|
||||||
(if (portfolio-holding-current-price holding)
|
|
||||||
(format "$%.2f" (portfolio-holding-current-price holding))
|
|
||||||
"...")
|
|
||||||
(format "$%.2f" (or (portfolio-holding-value holding) 0))
|
|
||||||
(propertize (format "$%.2f" (or (portfolio-holding-unrealized-gain holding) 0))
|
|
||||||
'face (if (>= (or (portfolio-holding-unrealized-gain holding) 0) 0)
|
|
||||||
'success 'error))
|
|
||||||
(format "$%.2f" (portfolio-holding-realized-gain holding))
|
|
||||||
(format "$%.2f" (portfolio-holding-dividends holding))
|
|
||||||
(propertize (format "%.1f%%" (or (portfolio-holding-unrealized-gain-percent holding) 0))
|
|
||||||
'face (if (>= (or (portfolio-holding-unrealized-gain-percent holding) 0) 0)
|
|
||||||
'success 'error)))))
|
|
||||||
portfolio-tracker-holdings))
|
|
||||||
(tabulated-list-print t)
|
|
||||||
;; Add summary at the bottom
|
|
||||||
(portfolio-tracker-display-summary)))))))
|
|
||||||
|
|
||||||
(defun portfolio-tracker-display-summary ()
|
|
||||||
"Display portfolio summary with totals at bottom of buffer."
|
|
||||||
(let ((inhibit-read-only t)
|
|
||||||
(total-cost 0)
|
|
||||||
(total-value 0)
|
|
||||||
(total-unrealized 0)
|
|
||||||
(total-realized 0)
|
|
||||||
(total-dividends 0))
|
|
||||||
;; Calculate totals
|
|
||||||
(dolist (holding portfolio-tracker-holdings)
|
|
||||||
(let ((cost (* (portfolio-holding-total-shares holding)
|
|
||||||
(portfolio-holding-average-cost holding))))
|
|
||||||
(setq total-cost (+ total-cost cost))
|
|
||||||
(setq total-value (+ total-value (or (portfolio-holding-value holding) cost)))
|
|
||||||
(setq total-unrealized (+ total-unrealized (or (portfolio-holding-unrealized-gain holding) 0)))
|
|
||||||
(setq total-realized (+ total-realized (portfolio-holding-realized-gain holding)))
|
|
||||||
(setq total-dividends (+ total-dividends (portfolio-holding-dividends holding)))))
|
|
||||||
|
|
||||||
;; Display summary
|
|
||||||
(goto-char (point-max))
|
|
||||||
(insert "\n" (make-string 100 ?-) "\n")
|
|
||||||
(insert (propertize "PORTFOLIO TOTALS\n" 'face 'bold))
|
|
||||||
(insert (format " Total Buy-In (Cost Basis): %s%.2f\n"
|
|
||||||
(propertize "$" 'face 'default)
|
|
||||||
total-cost))
|
|
||||||
(insert (format " Current Market Value: %s%.2f\n"
|
|
||||||
(propertize "$" 'face 'default)
|
|
||||||
total-value))
|
|
||||||
(insert (format " Unrealized Gain/Loss: %s\n"
|
|
||||||
(propertize (format "$%.2f (%.1f%%)"
|
|
||||||
total-unrealized
|
|
||||||
(if (> total-cost 0)
|
|
||||||
(* 100 (/ total-unrealized total-cost))
|
|
||||||
0))
|
|
||||||
'face (if (>= total-unrealized 0) 'success 'error))))
|
|
||||||
(insert (format " Realized Gain/Loss: %s\n"
|
|
||||||
(propertize (format "$%.2f" total-realized)
|
|
||||||
'face (if (>= total-realized 0) 'success 'error))))
|
|
||||||
(insert (format " Total Dividends Received: %s%.2f\n"
|
|
||||||
(propertize "$" 'face 'default)
|
|
||||||
total-dividends))
|
|
||||||
(insert (make-string 100 ?-) "\n")
|
|
||||||
(let ((total-gain (+ total-unrealized total-realized total-dividends))
|
|
||||||
(total-return-pct (if (> total-cost 0)
|
|
||||||
(* 100 (/ (+ total-unrealized total-dividends) total-cost))
|
|
||||||
0)))
|
|
||||||
(insert (format " TOTAL RETURN: %s\n"
|
|
||||||
(propertize (format "$%.2f (%.1f%%)"
|
|
||||||
total-gain
|
|
||||||
total-return-pct)
|
|
||||||
'face (if (>= total-gain 0) 'success 'error))))
|
|
||||||
(insert (make-string 100 ?=) "\n"))
|
|
||||||
(insert "\nKeys: [g] Refresh | [a] Add Transaction | [t] Show Transactions | [s] Save | [l] Load | [q] Quit\n")))
|
|
||||||
|
|
||||||
(defun portfolio-tracker-refresh ()
|
|
||||||
"Refresh prices and redisplay."
|
|
||||||
(interactive)
|
|
||||||
(portfolio-tracker-process-transactions)
|
|
||||||
(portfolio-tracker-update-prices)
|
|
||||||
(portfolio-tracker-refresh-display))
|
|
||||||
|
|
||||||
(defun portfolio-tracker-add-transaction ()
|
|
||||||
"Add a new transaction interactively."
|
|
||||||
(interactive)
|
|
||||||
(let* ((date (read-string "Date (YYYY-MM-DD): " (format-time-string "%Y-%m-%d")))
|
|
||||||
(type (intern (completing-read "Type: " '("buy" "sell" "dividend") nil t)))
|
|
||||||
(symbol (upcase (read-string "Symbol: ")))
|
|
||||||
(shares (read-number "Shares: "))
|
|
||||||
(price (read-number "Price per share: "))
|
|
||||||
(fees (read-number "Fees (0 if none): " 0))
|
|
||||||
(notes (read-string "Notes (optional): ")))
|
|
||||||
(push (make-portfolio-transaction
|
|
||||||
:date date
|
|
||||||
:type type
|
|
||||||
:symbol symbol
|
|
||||||
:shares shares
|
|
||||||
:price price
|
|
||||||
:fees fees
|
|
||||||
:notes notes)
|
|
||||||
portfolio-tracker-transactions)
|
|
||||||
(portfolio-tracker-process-transactions)
|
|
||||||
(portfolio-tracker-update-prices)))
|
|
||||||
|
|
||||||
(defun portfolio-tracker-show-transactions ()
|
|
||||||
"Show all transactions in a separate buffer."
|
|
||||||
(interactive)
|
|
||||||
(let ((buf (get-buffer-create "*Portfolio Transactions*")))
|
|
||||||
(with-current-buffer buf
|
|
||||||
(portfolio-transactions-mode)
|
|
||||||
(setq tabulated-list-entries
|
|
||||||
(mapcar (lambda (txn)
|
|
||||||
(list txn
|
|
||||||
(vector
|
|
||||||
(portfolio-transaction-date txn)
|
|
||||||
(symbol-name (portfolio-transaction-type txn))
|
|
||||||
(portfolio-transaction-symbol txn)
|
|
||||||
(format "%.2f" (portfolio-transaction-shares txn))
|
|
||||||
(format "$%.2f" (portfolio-transaction-price txn))
|
|
||||||
(format "$%.2f" (* (portfolio-transaction-shares txn)
|
|
||||||
(portfolio-transaction-price txn)))
|
|
||||||
(format "$%.2f" (or (portfolio-transaction-fees txn) 0))
|
|
||||||
(or (portfolio-transaction-notes txn) ""))))
|
|
||||||
(reverse portfolio-tracker-transactions)))
|
|
||||||
(tabulated-list-print))
|
|
||||||
(switch-to-buffer buf)))
|
|
||||||
|
|
||||||
(defun portfolio-tracker-save (file)
|
|
||||||
"Save portfolio data to FILE."
|
|
||||||
(interactive "FSave portfolio to: ")
|
|
||||||
(with-temp-file file
|
|
||||||
(insert ";; Portfolio Tracker Data\n")
|
|
||||||
(insert ";; Transaction history and settings\n\n")
|
|
||||||
(insert "(setq portfolio-tracker-transactions\n")
|
|
||||||
(pp portfolio-tracker-transactions (current-buffer))
|
|
||||||
(insert ")\n"))
|
|
||||||
(message "Portfolio saved to %s" file))
|
|
||||||
|
|
||||||
(defun portfolio-tracker-load (file)
|
|
||||||
"Load portfolio data from FILE."
|
|
||||||
(interactive "fLoad portfolio from: ")
|
|
||||||
(load-file file)
|
|
||||||
(portfolio-tracker-process-transactions)
|
|
||||||
(portfolio-tracker-refresh-display)
|
|
||||||
(portfolio-tracker-update-prices)
|
|
||||||
(message "Portfolio loaded from %s" file))
|
|
||||||
|
|
||||||
;;;###autoload
|
|
||||||
(defun portfolio-tracker ()
|
|
||||||
"Start the portfolio tracker."
|
|
||||||
(interactive)
|
|
||||||
(require 'tabulated-list)
|
|
||||||
(require 'cl-lib)
|
|
||||||
(let ((buf (get-buffer-create "*Portfolio Holdings*")))
|
|
||||||
(switch-to-buffer buf)
|
|
||||||
(unless (eq major-mode 'portfolio-tracker-mode)
|
|
||||||
(portfolio-tracker-mode))
|
|
||||||
(when portfolio-tracker-transactions
|
|
||||||
(portfolio-tracker-process-transactions)
|
|
||||||
(portfolio-tracker-refresh-display))
|
|
||||||
(message "Portfolio Tracker: 'a' add transaction | 't' show transactions | 'g' refresh prices | 'l' load portfolio")))
|
|
||||||
|
|
||||||
(provide 'portfolio-tracker-v2)
|
|
||||||
;;; portfolio-tracker-v2.el ends here
|
|
||||||
508
symbol_finder.py
508
symbol_finder.py
@@ -1,508 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
Reference in New Issue
Block a user