Remove unused files

This commit is contained in:
Jens Luedicke
2026-01-21 17:08:24 +01:00
parent ca74d93e60
commit b02a15f7e5
2 changed files with 0 additions and 959 deletions

View File

@@ -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

View File

@@ -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()