#SGrepMDI is by Gordon McMillan (gmcm@hypernet.com)
#It does basically what Find In Files does in MSVC with a couple enhancements.
# - It saves any directories in the app's ini file (if you want to get rid
#	of them you'll have to edit the file)
# - "Directories" can be directories, 
#  -	semicolon separated lists of "directories",
#  -	environment variables that evaluate to "directories",
#  -	registry path names that evaluate to "directories",
#  -	all of which is recursive, so you can mix them all up.
# - It is MDI, so you can 'nest' greps and return to earlier ones,
#	(ie, have multiple results open at the same time)
# - Like FIF, double clicking a line opens an editor and takes you to the line.
# - You can highlight text, right click and start a new grep with the selected
#	text as search pattern and same directories etc as before.
# - You can save grep parameters (so you don't lose your hardearned pattern)
#	from File|Save
# - You can save grep results by right clicking in the result window.
# Hats off to Mark Hammond for providing an environment where I could cobble
# something like this together in a couple evenings!

import win32ui
import win32api
from pywin.mfc import docview, dialog, window
import win32con
import string
import re
import glob
import os
import stat
import glob
import scriptutils

def getsubdirs(d):
	dlist = []
	flist = glob.glob(d+'\\*')
	for f in flist:
		if os.path.isdir(f):
			dlist.append(f)
			dlist = dlist + getsubdirs(f)
	return dlist

class dirpath:
	def __init__(self, str, recurse=0):
		dp = str.split(';')
		dirs = {}
		for d in dp:
			if os.path.isdir(d):
				d = d.lower()
				if d not in dirs:
					dirs[d] = None
					if recurse:
						subdirs = getsubdirs(d)
						for sd in subdirs:
							sd = sd.lower()
							if sd not in dirs:
								dirs[sd] = None
			elif os.path.isfile(d):
				pass
			else:
				x = None
				if d in os.environ:
					x = dirpath(os.environ[d])
				elif d[:5] == 'HKEY_':
					keystr = d.split('\\')
					try:
						root = eval('win32con.'+keystr[0])
					except:
						win32ui.MessageBox("Can't interpret registry key name '%s'" % keystr[0])
					try:
						subkey = '\\'.join(keystr[1:])
						val = win32api.RegQueryValue(root, subkey)
						if val:
							x = dirpath(val)
						else:
							win32ui.MessageBox("Registry path '%s' did not return a path entry" % d)
					except:
						win32ui.MessageBox("Can't interpret registry key value: %s" % keystr[1:])
				else:
					win32ui.MessageBox("Directory '%s' not found" % d)
				if x:
					for xd in x:
						if xd not in dirs:
							dirs[xd] = None
							if recurse:
								subdirs = getsubdirs(xd)
								for sd in subdirs:
									sd = sd.lower()
									if sd not in dirs:
										dirs[sd] = None
		self.dirs = []
		for d in dirs.keys():
			self.dirs.append(d)

	def __getitem__(self, key):
		return self.dirs[key]
	def __len__(self):
		return len(self.dirs)
	def __setitem__(self, key, value):
		self.dirs[key] = value
	def __delitem__(self, key):
		del self.dirs[key]
	def __getslice__(self, lo, hi):
		return self.dirs[lo:hi]
	def __setslice__(self, lo, hi, seq):
		self.dirs[lo:hi] = seq
	def __delslice__(self, lo, hi):
		del self.dirs[lo:hi]
	def __add__(self, other):
		if type(other) == type(self) or type(other) == type([]):
			return self.dirs + other.dirs
	def __radd__(self, other):
		if type(other) == type(self) or type(other) == type([]):
			return other.dirs + self.dirs

# Group(1) is the filename, group(2) is the lineno.
#regexGrepResult=regex.compile("^\\([a-zA-Z]:.*\\)(\\([0-9]+\\))")

regexGrep=re.compile(r"^([a-zA-Z]:[^(]*)\(([0-9]+)\)")

#these are the atom numbers defined by Windows for basic dialog controls

BUTTON    = 0x80
EDIT      = 0x81
STATIC    = 0x82
LISTBOX   = 0x83
SCROLLBAR = 0x84
COMBOBOX  = 0x85

class GrepTemplate(docview.RichEditDocTemplate):
	def __init__(self):
		docview.RichEditDocTemplate.__init__(self, win32ui.IDR_TEXTTYPE, GrepDocument, GrepFrame, GrepView)
		self.SetDocStrings("\nGrep\nGrep\nGrep params (*.grep)\n.grep\n\n\n")
		win32ui.GetApp().AddDocTemplate(self)
		self.docparams = None

	def MatchDocType(self, fileName, fileType):
		doc = self.FindOpenDocument(fileName)
		if doc: return doc
		ext = os.path.splitext(fileName)[1].lower()
		if ext =='.grep': 
			return win32ui.CDocTemplate_Confidence_yesAttemptNative
		return win32ui.CDocTemplate_Confidence_noAttempt

	def setParams(self, params):
		self.docparams = params

	def readParams(self):
		tmp = self.docparams
		self.docparams = None
		return tmp

class GrepFrame(window.MDIChildWnd):
	# The template and doc params will one day be removed.
	def __init__(self, wnd = None):
		window.MDIChildWnd.__init__(self, wnd)

class GrepDocument(docview.RichEditDoc):
	def __init__(self, template):
		docview.RichEditDoc.__init__(self, template)
		self.dirpattern = ''
		self.filpattern = ''
		self.greppattern = ''
		self.casesensitive = 1
		self.recurse = 1
		self.verbose = 0

	def OnOpenDocument(self, fnm):
		#this bizarre stuff with params is so right clicking in a result window
		#and starting a new grep can communicate the default parameters to the
		#new grep.
		try:
			params = open(fnm,'r').read()
		except:
			params = None
		self.setInitParams(params)
		return self.OnNewDocument()

	def OnCloseDocument(self):
		try:
			win32ui.GetApp().DeleteIdleHandler(self.SearchFile)
		except:
			pass
		return self._obj_.OnCloseDocument()

	def saveInitParams(self):
		# Only save the flags, not the text boxes.
		paramstr = "\t%s\t\t%d\t%d" % (self.filpattern, self.casesensitive, self.recurse)
		win32ui.WriteProfileVal("Grep", "Params", paramstr)

	def setInitParams(self, paramstr):
		if paramstr is None:
			paramstr = win32ui.GetProfileVal("Grep", "Params", '\t\t\t1\t0\t0')
		params = paramstr.split('\t')
		if len(params) < 3:
			params = params + ['']*(3-len(params))
		if len(params) < 6:
			params = params + [0]*(6-len(params))
		self.dirpattern = params[0]
		self.filpattern = params[1]
		self.greppattern = params[2]
		self.casesensitive = int(params[3])
		self.recurse = int(params[4])
		self.verbose = int(params[5])
		# setup some reasonable defaults.
		if not self.dirpattern:
			try: 
				editor=win32ui.GetMainFrame().MDIGetActive()[0].GetEditorView()
				self.dirpattern=os.path.abspath(os.path.dirname(editor.GetDocument().GetPathName()))
			except (AttributeError, win32ui.error):
				self.dirpattern = os.getcwd()
		if not self.filpattern:
			self.filpattern = "*.py"

	def OnNewDocument(self):
		if self.dirpattern == '':
			self.setInitParams(greptemplate.readParams())
		d = GrepDialog(self.dirpattern, self.filpattern, self.greppattern, self.casesensitive, self.recurse, self.verbose)
		if d.DoModal() == win32con.IDOK:
			self.dirpattern = d['dirpattern']
			self.filpattern = d['filpattern']
			self.greppattern = d['greppattern']
			self.casesensitive = d['casesensitive']
			self.recurse = d['recursive']
			self.verbose = d['verbose']
			self.doSearch()
			self.saveInitParams()
			return 1
		return 0 # cancelled - return zero to stop frame creation.

	def doSearch(self):
		self.dp = dirpath(self.dirpattern, self.recurse)
		self.SetTitle("Grep for %s in %s" % (self.greppattern, self.filpattern))
		#self.text = []
		self.GetFirstView().Append('#Search '+self.dirpattern+'\n')
		if self.verbose:
			self.GetFirstView().Append('#   ='+repr(self.dp.dirs)+'\n')
		self.GetFirstView().Append('# Files '+self.filpattern+'\n')
		self.GetFirstView().Append('#   For '+self.greppattern+'\n')
		self.fplist = self.filpattern.split(';')
		if self.casesensitive:
			self.pat = re.compile(self.greppattern)
		else:
			self.pat = re.compile(self.greppattern, re.IGNORECASE)
		win32ui.SetStatusText("Searching.  Please wait...", 0)
		self.dpndx = self.fpndx = 0
		self.fndx = -1
		if not self.dp:
			self.GetFirstView().Append("# ERROR: '%s' does not resolve to any search locations" % self.dirpattern)
			self.SetModifiedFlag(0)
		else:
			self.flist = glob.glob(self.dp[0]+'\\'+self.fplist[0])
			win32ui.GetApp().AddIdleHandler(self.SearchFile)

	def SearchFile(self, handler, count):
		self.fndx = self.fndx + 1
		if self.fndx < len(self.flist):
			f = self.flist[self.fndx]
			if self.verbose:
				self.GetFirstView().Append('# ..'+f+'\n')
			# Directories may match the file type pattern, and files may be removed
			#  while grep is running
			if os.path.isfile(f):
				win32ui.SetStatusText("Searching "+f, 0)
				lines = open(f, 'r').readlines()
				for i in range(len(lines)):
					line = lines[i]
					if self.pat.search(line) != None:
						self.GetFirstView().Append(f+'('+repr(i+1) + ') '+line)
		else:
			self.fndx = -1
			self.fpndx = self.fpndx + 1
			if self.fpndx < len(self.fplist):
				self.flist = glob.glob(self.dp[self.dpndx] + '\\' + self.fplist[self.fpndx])
			else:
				self.fpndx = 0
				self.dpndx = self.dpndx + 1
				if self.dpndx < len(self.dp):
					self.flist = glob.glob(self.dp[self.dpndx] + '\\' + self.fplist[self.fpndx])
				else:
					win32ui.SetStatusText("Search complete.", 0)
					self.SetModifiedFlag(0) # default to not modified.
					try:
						win32ui.GetApp().DeleteIdleHandler(self.SearchFile)
					except:
						pass
					return 0
		return 1

	def GetParams(self):
		return self.dirpattern+'\t'+self.filpattern+'\t'+self.greppattern+'\t'+repr(self.casesensitive)+'\t'+repr(self.recurse)+'\t'+repr(self.verbose)

	def OnSaveDocument(self, filename):
#		print 'OnSaveDocument() filename=',filename
		savefile = open(filename,"wb")
		txt = self.GetParams()+'\n'
#		print 'writing',txt
		savefile.write(txt)
		savefile.close()
		self.SetModifiedFlag(0)
		return 1

ID_OPEN_FILE = 0xe400
ID_GREP	 = 0xe401
ID_SAVERESULTS = 0x402
ID_TRYAGAIN = 0x403

class GrepView(docview.RichEditView):
	def __init__(self, doc):
		docview.RichEditView.__init__(self, doc)
		self.SetWordWrap(win32ui.CRichEditView_WrapNone)
		self.HookHandlers()

	def OnInitialUpdate(self):
		rc = self._obj_.OnInitialUpdate()
		format = (-402653169, 0, 200, 0, 0, 0, 49, 'Courier New')
		self.SetDefaultCharFormat(format)
		return rc

	def HookHandlers(self):
		self.HookMessage(self.OnRClick, win32con.WM_RBUTTONDOWN)
		self.HookCommand(self.OnCmdOpenFile, ID_OPEN_FILE)
		self.HookCommand(self.OnCmdGrep, ID_GREP)
		self.HookCommand(self.OnCmdSave, ID_SAVERESULTS)
		self.HookCommand(self.OnTryAgain, ID_TRYAGAIN)
		self.HookMessage(self.OnLDblClick,win32con.WM_LBUTTONDBLCLK)

	def OnLDblClick(self,params):
		line = self.GetLine()
		regexGrepResult = regexGrep.match(line)
		if regexGrepResult:
			fname = regexGrepResult.group(1)
			line = int(regexGrepResult.group(2))
			scriptutils.JumpToDocument(fname, line)
			return 0	# dont pass on
		return 1	# pass it on by default.

	def OnRClick(self, params):
		menu = win32ui.CreatePopupMenu()
		flags=win32con.MF_STRING|win32con.MF_ENABLED
		lineno = self._obj_.LineFromChar(-1)	#selection or current line
		line = self._obj_.GetLine(lineno)
		regexGrepResult = regexGrep.match(line)
		if regexGrepResult:
			self.fnm = regexGrepResult.group(1)
			self.lnnum = int(regexGrepResult.group(2))
			menu.AppendMenu(flags, ID_OPEN_FILE, "&Open "+self.fnm)
			menu.AppendMenu(win32con.MF_SEPARATOR)
		menu.AppendMenu(flags, ID_TRYAGAIN, "&Try Again")
		charstart, charend = self._obj_.GetSel()
		if charstart != charend:
			linestart = self._obj_.LineIndex(lineno)
			self.sel = line[charstart-linestart:charend-linestart]
			menu.AppendMenu(flags, ID_GREP, "&Grep for "+self.sel)
			menu.AppendMenu(win32con.MF_SEPARATOR)
		menu.AppendMenu(flags, win32ui.ID_EDIT_CUT, 'Cu&t')
		menu.AppendMenu(flags, win32ui.ID_EDIT_COPY, '&Copy')
		menu.AppendMenu(flags, win32ui.ID_EDIT_PASTE, '&Paste')
		menu.AppendMenu(flags, win32con.MF_SEPARATOR);
		menu.AppendMenu(flags, win32ui.ID_EDIT_SELECT_ALL, '&Select all')
		menu.AppendMenu(flags, win32con.MF_SEPARATOR);
		menu.AppendMenu(flags, ID_SAVERESULTS, 'Sa&ve results')
		menu.TrackPopupMenu(params[5])
		return 0

	def OnCmdOpenFile(self, cmd, code):
		doc = win32ui.GetApp().OpenDocumentFile(self.fnm)
		if doc:
			vw = doc.GetFirstView()
			#hope you have an editor that implements GotoLine()!
			try:
				vw.GotoLine(int(self.lnnum))
			except:
				pass
		return 0

	def OnCmdGrep(self, cmd, code):
		curparamsstr = self.GetDocument().GetParams()
		params = curparamsstr.split('\t')
		params[2] = self.sel
		greptemplate.setParams('\t'.join(params))
		greptemplate.OpenDocumentFile()
		return 0
	
	def OnTryAgain(self, cmd, code):
		greptemplate.setParams(self.GetDocument().GetParams())
		greptemplate.OpenDocumentFile()
		return 0

	def OnCmdSave(self, cmd, code):
		flags = win32con.OFN_OVERWRITEPROMPT 
		dlg = win32ui.CreateFileDialog(0, None, None, flags, "Text Files (*.txt)|*.txt||", self)
		dlg.SetOFNTitle("Save Results As")
		if dlg.DoModal() == win32con.IDOK:
			pn = dlg.GetPathName()
			self._obj_.SaveTextFile(pn)
		return 0
		
	def Append(self, strng):
		numlines = self.GetLineCount()
		endpos = self.LineIndex(numlines-1) + len(self.GetLine(numlines-1))
		self.SetSel(endpos, endpos)
		self.ReplaceSel(strng)


class GrepDialog(dialog.Dialog):
	def __init__(self, dp, fp, gp, cs, r, v):
		style = win32con.DS_MODALFRAME | win32con.WS_POPUP | win32con.WS_VISIBLE | win32con.WS_CAPTION | win32con.WS_SYSMENU | win32con.DS_SETFONT
		CS = win32con.WS_CHILD | win32con.WS_VISIBLE
		tmp = [ ["Grep", (0, 0, 210, 90), style, None, (8, "MS Sans Serif")], ]
		tmp.append([STATIC, "Grep For:",            -1, (7,   7,  50,  9), CS ])
		tmp.append([EDIT,   gp,                    101, (52,  7, 144,  11), CS | win32con.WS_TABSTOP | win32con.ES_AUTOHSCROLL | win32con.WS_BORDER])
		tmp.append([STATIC, "Directories:",         -1, (7,  20,  50,  9), CS ])
		tmp.append([EDIT,   dp,                    102, (52, 20, 128,  11), CS | win32con.WS_TABSTOP | win32con.ES_AUTOHSCROLL | win32con.WS_BORDER])
		tmp.append([BUTTON, '...',                 110, (182,20,  16,  11), CS | win32con.BS_PUSHBUTTON | win32con.WS_TABSTOP]) 
		tmp.append([STATIC, "File types:",          -1, (7,  33,  50,  9), CS ])
		tmp.append([EDIT,   fp,                    103, (52, 33, 128,  11), CS | win32con.WS_TABSTOP | win32con.ES_AUTOHSCROLL | win32con.WS_BORDER ])
		tmp.append([BUTTON, '...',                 111, (182,33,  16,  11), CS | win32con.BS_PUSHBUTTON | win32con.WS_TABSTOP]) 
		tmp.append([BUTTON,'Case sensitive',       104, (7,  45,  72,  9), CS | win32con.BS_AUTOCHECKBOX | win32con.BS_LEFTTEXT| win32con.WS_TABSTOP])
		tmp.append([BUTTON,'Subdirectories',       105, (7,  56,  72,  9), CS | win32con.BS_AUTOCHECKBOX | win32con.BS_LEFTTEXT| win32con.WS_TABSTOP])
		tmp.append([BUTTON,'Verbose',              106, (7,  67,  72,  9), CS | win32con.BS_AUTOCHECKBOX | win32con.BS_LEFTTEXT| win32con.WS_TABSTOP])
		tmp.append([BUTTON,'OK',         win32con.IDOK, (166,53,  32, 12), CS | win32con.BS_DEFPUSHBUTTON| win32con.WS_TABSTOP])
		tmp.append([BUTTON,'Cancel', win32con.IDCANCEL, (166,67,  32, 12), CS | win32con.BS_PUSHBUTTON| win32con.WS_TABSTOP])
		dialog.Dialog.__init__(self, tmp)
		self.AddDDX(101,'greppattern')
		self.AddDDX(102,'dirpattern')
		self.AddDDX(103,'filpattern')
		self.AddDDX(104,'casesensitive')
		self.AddDDX(105,'recursive')
		self.AddDDX(106,'verbose')
		self._obj_.data['greppattern'] = gp
		self._obj_.data['dirpattern']  = dp
		self._obj_.data['filpattern']  = fp
		self._obj_.data['casesensitive']  = cs
		self._obj_.data['recursive'] = r
		self._obj_.data['verbose']  = v
		self.HookCommand(self.OnMoreDirectories, 110)
		self.HookCommand(self.OnMoreFiles, 111)

	def OnMoreDirectories(self, cmd, code):
		self.getMore('Grep\\Directories', 'dirpattern')

	def OnMoreFiles(self, cmd, code):
		self.getMore('Grep\\File Types', 'filpattern')
		
	def getMore(self, section, key):
		self.UpdateData(1)
		#get the items out of the ini file
		ini = win32ui.GetProfileFileName()
		secitems = win32api.GetProfileSection(section, ini)
		items = []
		for secitem in secitems:
			items.append(secitem.split('=')[1])
		dlg = GrepParamsDialog(items)
		if dlg.DoModal() == win32con.IDOK:
			itemstr = ';'.join(dlg.getItems())
			self._obj_.data[key] = itemstr
			#update the ini file with dlg.getNew()
			i = 0
			newitems = dlg.getNew()
			if newitems:
				items = items + newitems
				for item in items:
					win32api.WriteProfileVal(section, repr(i), item, ini)
					i = i + 1
			self.UpdateData(0)

	def OnOK(self):
		self.UpdateData(1)
		for id, name in [(101,'greppattern'), (102,'dirpattern'), (103,'filpattern')]:
			if not self[name]:
				self.GetDlgItem(id).SetFocus()
				win32api.MessageBeep()
				win32ui.SetStatusText("Please enter a value")
				return
		self._obj_.OnOK()

class GrepParamsDialog(dialog.Dialog):
	def __init__(self, items):
		self.items = items
		self.newitems = []
		style = win32con.DS_MODALFRAME | win32con.WS_POPUP | win32con.WS_VISIBLE | win32con.WS_CAPTION | win32con.WS_SYSMENU | win32con.DS_SETFONT
		CS = win32con.WS_CHILD | win32con.WS_VISIBLE
		tmp = [ ["Grep Parameters", (0, 0, 205, 100), style, None, (8, "MS Sans Serif")], ]
		tmp.append([LISTBOX, '',                   107, (7,   7,  150,  72), CS | win32con.LBS_MULTIPLESEL| win32con.LBS_STANDARD | win32con.LBS_HASSTRINGS | win32con.WS_TABSTOP | win32con.LBS_NOTIFY])
		tmp.append([BUTTON,'OK',         win32con.IDOK, (167, 7,  32, 12), CS | win32con.BS_DEFPUSHBUTTON| win32con.WS_TABSTOP])
		tmp.append([BUTTON,'Cancel', win32con.IDCANCEL, (167,23,  32, 12), CS | win32con.BS_PUSHBUTTON| win32con.WS_TABSTOP])
		tmp.append([STATIC,'New:',                  -1, (2,  83,  15,  12), CS])
		tmp.append([EDIT,  '',                     108, (18, 83,  139,  12), CS | win32con.WS_TABSTOP | win32con.ES_AUTOHSCROLL | win32con.WS_BORDER])
		tmp.append([BUTTON,'Add',                  109, (167,83,  32, 12), CS | win32con.BS_PUSHBUTTON| win32con.WS_TABSTOP]) 
		dialog.Dialog.__init__(self, tmp)
		self.HookCommand(self.OnAddItem, 109)
		self.HookCommand(self.OnListDoubleClick, 107)

	def OnInitDialog(self):
		lb = self.GetDlgItem(107)
		for item in self.items:
			lb.AddString(item)
		return self._obj_.OnInitDialog()

	def OnAddItem(self, cmd, code):
		eb = self.GetDlgItem(108)
		item = eb.GetLine(0)
		self.newitems.append(item)
		lb = self.GetDlgItem(107)
		i = lb.AddString(item)
		lb.SetSel(i, 1)
		return 1

	def OnListDoubleClick(self, cmd, code):
		if code == win32con.LBN_DBLCLK:
			self.OnOK()
			return 1

	def OnOK(self):
		lb = self.GetDlgItem(107)
		self.selections = lb.GetSelTextItems()
		self._obj_.OnOK()

	def getItems(self):
		return self.selections

	def getNew(self):
		return self.newitems

try:
	win32ui.GetApp().RemoveDocTemplate(greptemplate)
except NameError:
	pass

greptemplate = GrepTemplate() 
