git/git-gui/lib/search.tcl

# incremental search panel
# based on code from gitk, Copyright (C) Paul Mackerras

class searchbar {

field w
field ctext

field searchstring   {}
field regexpsearch
field default_regexpsearch
field casesensitive
field default_casesensitive
field smartcase
field searchdirn     -forwards

field history
field history_index

field smarktop
field smarkbot

constructor new {i_w i_text args} {
	global use_ttk NS
	set w      $i_w
	set ctext  $i_text

	set default_regexpsearch [is_config_true gui.search.regexp]
	switch -- [get_config gui.search.case] {
	no {
		set default_casesensitive 0
		set smartcase 0
	}
	smart {
		set default_casesensitive 0
		set smartcase 1
	}
	yes -
	default {
		set default_casesensitive 1
		set smartcase 0
	}
	}

	set history [list]

	${NS}::frame  $w
	${NS}::label  $w.l       -text [mc Find:]
	tentry  $w.ent -textvariable ${__this}::searchstring -background lightgreen
	${NS}::button $w.bn      -text [mc Next] -command [cb find_next]
	${NS}::button $w.bp      -text [mc Prev] -command [cb find_prev]
	${NS}::checkbutton $w.re -text [mc RegExp] \
		-variable ${__this}::regexpsearch -command [cb _incrsearch]
	${NS}::checkbutton $w.cs -text [mc Case] \
		-variable ${__this}::casesensitive -command [cb _incrsearch]
	pack   $w.l   -side left
	pack   $w.cs  -side right
	pack   $w.re  -side right
	pack   $w.bp  -side right
	pack   $w.bn  -side right
	pack   $w.ent -side left -expand 1 -fill x

	eval grid conf $w -sticky we $args
	grid remove $w

	trace add variable searchstring write [cb _incrsearch_cb]
	bind $w.ent <Return> [cb find_next]
	bind $w.ent <Shift-Return> [cb find_prev]
	bind $w.ent <Key-Up>   [cb _prev_search]
	bind $w.ent <Key-Down> [cb _next_search]
	
	bind $w <Destroy> [list delete_this $this]
	return $this
}

method show {} {
	if {![visible $this]} {
		grid $w
		$w.ent delete 0 end
		set regexpsearch  $default_regexpsearch
		set casesensitive $default_casesensitive
		set history_index [llength $history]
	}
	focus -force $w.ent
}

method hide {} {
	if {[visible $this]} {
		focus $ctext
		grid remove $w
		_save_search $this
	}
}

method visible {} {
	return [winfo ismapped $w]
}

method editor {} {
	return $w.ent
}

method _get_new_anchor {} {
	# use start of selection if it is visible,
	# or the bounds of the visible area
	set top    [$ctext index @0,0]
	set bottom [$ctext index @0,[winfo height $ctext]]
	set sel    [$ctext tag ranges sel]
	if {$sel ne {}} {
		set spos [lindex $sel 0]
		if {[lindex $spos 0] >= [lindex $top 0] &&
		    [lindex $spos 0] <= [lindex $bottom 0]} {
			return $spos
		}
	}
	if {$searchdirn eq "-forwards"} {
		return $top
	} else {
		return $bottom
	}
}

method _get_wrap_anchor {dir} {
	if {$dir eq "-forwards"} {
		return 1.0
	} else {
		return end
	}
}

method _do_search {start {mlenvar {}} {dir {}} {endbound {}}} {
	set cmd [list $ctext search]
	if {$mlenvar ne {}} {
		upvar $mlenvar mlen
		lappend cmd -count mlen
	}
	if {$regexpsearch} {
		lappend cmd -regexp
	}
	if {!$casesensitive} {
		lappend cmd -nocase
	}
	if {$dir eq {}} {
		set dir $searchdirn
	}
	lappend cmd $dir -- $searchstring
	if {[catch {
		if {$endbound ne {}} {
			set here [eval $cmd [list $start] [list $endbound]]
		} else {
			set here [eval $cmd [list $start]]
			if {$here eq {}} {
				set here [eval $cmd [_get_wrap_anchor $this $dir]]
			}
		}
	} err]} { set here {} }
	return $here
}

method _incrsearch_cb {name ix op} {
	after idle [cb _incrsearch]
}

method _incrsearch {} {
	$ctext tag remove found 1.0 end
	if {[catch {$ctext index anchor}]} {
		$ctext mark set anchor [_get_new_anchor $this]
	}
	if {$searchstring ne {}} {
		if {$smartcase && [regexp {[[:upper:]]} $searchstring]} {
			set casesensitive 1
		}
		set here [_do_search $this anchor mlen]
		if {$here ne {}} {
			$ctext see $here
			$ctext tag remove sel 1.0 end
			$ctext tag add sel $here "$here + $mlen c"
			#$w.ent configure -background lightgreen
			$w.ent state !pressed
			_set_marks $this 1
		} else {
			#$w.ent configure -background lightpink
			$w.ent state pressed
		}
	} elseif {$smartcase} {
		# clearing the field resets the smart case detection
		set casesensitive 0
	}
}

method _save_search {} {
	if {$searchstring eq {}} {
		return
	}
	if {[llength $history] > 0} {
		foreach {s_regexp s_case s_expr} [lindex $history end] break
	} else {
		set s_regexp $regexpsearch
		set s_case   $casesensitive
		set s_expr   ""
	}
	if {$searchstring eq $s_expr} {
		# update modes
		set history [lreplace $history end end \
				[list $regexpsearch $casesensitive $searchstring]]
	} else {
		lappend history [list $regexpsearch $casesensitive $searchstring]
	}
	set history_index [llength $history]
}

method _prev_search {} {
	if {$history_index > 0} {
		incr history_index -1
		foreach {s_regexp s_case s_expr} [lindex $history $history_index] break
		$w.ent delete 0 end
		$w.ent insert 0 $s_expr
		set regexpsearch $s_regexp
		set casesensitive $s_case
	}
}

method _next_search {} {
	if {$history_index < [llength $history]} {
		incr history_index
	}
	if {$history_index < [llength $history]} {
		foreach {s_regexp s_case s_expr} [lindex $history $history_index] break
	} else {
		set s_regexp $default_regexpsearch
		set s_case   $default_casesensitive
		set s_expr   ""
	}
	$w.ent delete 0 end
	$w.ent insert 0 $s_expr
	set regexpsearch $s_regexp
	set casesensitive $s_case
}

method find_prev {} {
	find_next $this -backwards
}

method find_next {{dir -forwards}} {
	focus $w.ent
	$w.ent icursor end
	set searchdirn $dir
	$ctext mark unset anchor
	if {$searchstring ne {}} {
		_save_search $this
		set start [_get_new_anchor $this]
		if {$dir eq "-forwards"} {
			set start "$start + 1c"
		}
		set match [_do_search $this $start mlen]
		$ctext tag remove sel 1.0 end
		if {$match ne {}} {
			$ctext see $match
			$ctext tag add sel $match "$match + $mlen c"
		}
	}
}

method _mark_range {first last} {
	set mend $first.0
	while {1} {
		set match [_do_search $this $mend mlen -forwards $last.end]
		if {$match eq {}} break
		set mend "$match + $mlen c"
		$ctext tag add found $match $mend
	}
}

method _set_marks {doall} {
	set topline [lindex [split [$ctext index @0,0] .] 0]
	set botline [lindex [split [$ctext index @0,[winfo height $ctext]] .] 0]
	if {$doall || $botline < $smarktop || $topline > $smarkbot} {
		# no overlap with previous
		_mark_range $this $topline $botline
		set smarktop $topline
		set smarkbot $botline
	} else {
		if {$topline < $smarktop} {
			_mark_range $this $topline [expr {$smarktop-1}]
			set smarktop $topline
		}
		if {$botline > $smarkbot} {
			_mark_range $this [expr {$smarkbot+1}] $botline
			set smarkbot $botline
		}
	}
}

method scrolled {} {
	if {$searchstring ne {}} {
		after idle [cb _set_marks 0]
	}
}

}