MediaWiki r33400 - Code Review

Jump to: navigation, search
Repository:MediaWiki
Revision:r33399‎ | r33400 (on ViewVC)‎ | r33401 >
Date:23:06, 15 April 2008
Author:rainman
Status:old (Comments)
Tags:
Comment:
Ajax suggestions:
* check in a new ajax suggestion engine (mwsuggest.js) which uses
OpenSearch to fetch results (by default via API), this should
deprecated the old ajaxsearch thingy
* extend PrefixSearchBackend hook to accept multiple namespaces for
future lucene use (default implementation however can still
process only one)
* Added to preferences, also a feature to turn it on/off for every
input (disabled atm until I work out browser issues completely)
* WMF wikis probably won't be using API to fetch results, but a
custom php wrapper that just forwards the request to appropriate
lucene daemon, added support for that

SpecialSearch:
* moved stuff out of SpecialSearch to SearchEngine, like snippet
highlighting and such
* support for additional interwiki results, e.g. title matches
from other projects shown in a separate box on the right
* todo: interwiki box doesn't have standard prev/next links to
avoid clutter and unintuitive interface
* support for related articles
Modified paths:

Diff [purge]

Index: trunk/phase3/maintenance/language/messages.inc
===================================================================
--- trunk/phase3/maintenance/language/messages.inc	(revision 33399)
+++ trunk/phase3/maintenance/language/messages.inc	(revision 33400)
@@ -701,6 +701,15 @@
 		'search-redirect',
 		'search-section',
 		'search-suggest',
+		'search-interwiki-caption',
+		'search-interwiki-default',
+		'search-interwiki-custom',
+		'search-interwiki-more',
+		'search-mwsuggest-enabled',
+		'search-mwsuggest-disabled',	
+		'search-relatedarticle',
+		'mwsuggest-disable',
+		'searchrelated',	
 		'searchall',
 		'showingresults',
 		'showingresultsnum',
Index: trunk/phase3/skins/monobook/main.css
===================================================================
--- trunk/phase3/skins/monobook/main.css	(revision 33399)
+++ trunk/phase3/skins/monobook/main.css	(revision 33400)
@@ -1167,7 +1167,7 @@
 	font-size: 75%;
 	text-align: right;
 }
-span.newpage, span.minor, span.searchmatch, span.bot {
+span.newpage, span.minor, span.bot {
 	font-weight: bold;
 }
 span.unpatrolled {
@@ -1175,24 +1175,6 @@
 	color: red;
 }
 
-span.searchmatch {
-	font-size: 95%;	
-}
-div.searchresult {
-	font-size: 95%;
-	width:38em;
-}
-
-span.searchalttitle {
-	font-size: 95%;
-}
-
-div.searchdidyoumean {
-	font-size: 127%;
-	padding-bottom:1ex;
-	padding-top:1ex; 
-}
-
 .sharedUploadNotice {
 	font-style: italic;
 }
@@ -1562,3 +1544,25 @@
 	vertical-align: middle;
 	font-size: 90%;
 }
+
+/** Special:Search stuff */
+div#mw-search-interwiki-caption {
+	text-align: center;
+	font-weight: bold;
+	font-size: 95%;
+}
+
+.mw-search-interwiki-project {
+	font-size: 97%;
+	text-align: left;
+	padding-left: 0.2em;
+	padding-right: 0.15em;
+	padding-bottom: 0.2em;
+	padding-top: 0.15em;
+	background: #cae8ff;
+}
+
+span.searchmatch {
+	font-weight: bold;
+}
+
Index: trunk/phase3/skins/common/shared.css
===================================================================
--- trunk/phase3/skins/common/shared.css	(revision 33399)
+++ trunk/phase3/skins/common/shared.css	(revision 33400)
@@ -91,13 +91,46 @@
 }
 
 /* Search results */
+div.searchresult {
+	font-size: 95%;
+	width:38em;
+}
 .mw-search-results li {
 	padding-bottom: 1em;
 }
 .mw-search-result-data {
 	color: green;
+	font-size: 97%;
 }
 
+div#mw-search-interwiki {
+	float: right;
+	width: 18em;
+	border-style: solid;
+	border-color: #AAAAAA;
+	border-width: 1px;
+	margin-top: 2ex;
+}
+
+div#mw-search-interwiki li {
+	font-size: 95%;
+}
+
+.mw-search-interwiki-more {
+	float: right;
+	font-size: 90%;
+}
+
+span.searchalttitle {
+	font-size: 95%;
+}
+
+div.searchdidyoumean {
+	font-size: 127%;
+	padding-bottom:1ex;
+	padding-top:1ex; 
+}
+
 /*
  * UserRights stuff
  */
@@ -109,6 +142,53 @@
 	padding-right: 1.5em;
 }
 
+/* 
+ * OpenSearch ajax suggestions
+ */
+.os-suggest {
+	overflow: auto; 
+	overflow-x: hidden; 
+	position: absolute;
+	top: 0px;
+	left: 0px;
+	width: 0px;
+	background-color: white; 
+	border-style: solid;
+	border-color: #AAAAAA;
+	border-width: 1px;
+	z-index:99; 
+	visibility:hidden; 
+	font-size:95%;	
+}
+
+table.os-suggest-results {
+	font-size: 95%;
+	cursor: pointer; 
+	border: 0; 
+}
+
+td.os-suggest-result, td.os-suggest-result-hl {
+	align: left; 
+	white-space: nowrap;
+	background-color: white; 
+}
+td.os-suggest-result-hl {
+	background-color: #4C59A6; 
+	color: white;
+}
+.os-suggest-toggle {
+	position: relative; 
+	left: 1ex;
+	font-size: 65%;
+}
+.os-suggest-toggle-def {
+	position: absolute;
+	top: 0px;
+	left: 0px;
+	font-size: 65%;
+	visibility: hidden;
+}
+
 /* Page history styling */
 /* the auto-generated edit comments */
 .autocomment { color: gray; }
Index: trunk/phase3/skins/common/mwsuggest.js
===================================================================
--- trunk/phase3/skins/common/mwsuggest.js	(revision 0)
+++ trunk/phase3/skins/common/mwsuggest.js	(revision 33400)
@@ -0,0 +1,762 @@
+/*
+ * OpenSearch ajax suggestion engine for MediaWiki
+ * 
+ * uses core MediaWiki open search support to fetch suggestions
+ * and show them below search boxes and other inputs
+ *
+ * by Robert Stojnic (April 2008)
+ */
+ 
+// search_box_id -> Results object 
+var os_map = {};
+// cached data, url -> json_text
+var os_cache = {};
+// global variables for suggest_keypress
+var os_cur_keypressed = 0;
+var os_last_keypress = 0;
+var os_keypressed_count = 0;
+// type: Timer
+var os_timer = null;
+// tie mousedown/up events
+var os_mouse_pressed = false;
+var os_mouse_num = -1;
+// if true, the last change was made by mouse (and not keyboard)
+var os_mouse_moved = false;
+// delay between keypress and suggestion (in ms)
+var os_search_timeout = 250;
+// these pairs of inputs/forms will be autoloaded at startup
+var os_autoload_inputs = new Array('searchInput', 'powerSearchText', 'searchText');
+var os_autoload_forms = new Array('searchform', 'powersearch', 'search' );
+// if we stopped the service
+var os_is_stopped = false;
+// max lines to show in suggest table
+var os_max_lines_per_suggest = 7;
+// if we are about to focus the searchbox for the first time
+var os_first_focus = true;
+
+/** Timeout timer class that will fetch the results */ 
+function os_Timer(id,r,query){
+	this.id = id;
+	this.r = r;
+	this.query = query;	
+}
+
+/** Property class for single search box */
+function os_Results(name, formname){	
+	this.searchform = formname; // id of the searchform
+	this.searchbox = name; // id of the searchbox
+	this.container = name+"Suggest"; // div that holds results
+	this.resultTable = name+"Result"; // id base for the result table (+num = table row)
+	this.resultText = name+"ResultText"; // id base for the spans within result tables (+num)
+	this.toggle = name+"Toggle"; // div that has the toggle (enable/disable) link
+	this.query = null; // last processed query
+	this.results = null;  // parsed titles
+	this.resultCount = 0; // number of results
+	this.original = null; // query that user entered 
+	this.selected = -1; // which result is selected
+	this.containerCount = 0; // number of results visible in container 
+	this.containerRow = 0; // height of result field in the container
+	this.containerTotal = 0; // total height of the container will all results
+	this.visible = false; // if container is visible
+}
+
+/** Hide results div */
+function os_hideResults(r){
+	var c = document.getElementById(r.container);
+	if(c != null)
+		c.style.visibility = "hidden";
+	r.visible = false;
+	r.selected = -1;
+}
+
+/** Show results div */
+function os_showResults(r){
+	if(os_is_stopped)
+		return;
+	os_fitContainer(r);
+	var c = document.getElementById(r.container);
+	r.selected = -1;
+	if(c != null){
+		c.scrollTop = 0;
+		c.style.visibility = "visible";
+		r.visible = true;
+	}	
+}
+
+function os_operaWidthFix(x){
+	// TODO: better css2 incompatibility detection here
+	if(is_opera || is_khtml || navigator.userAgent.toLowerCase().indexOf('firefox/1')!=-1){
+		return x - 30; // opera&konqueror & old firefox don't understand overflow-x, estimate scrollbar width
+	}	
+	return x;
+}
+
+function os_encodeQuery(value){
+  if (encodeURIComponent) {
+    return encodeURIComponent(value);
+  }
+  if(escape) {
+    return escape(value);
+  }
+}
+function os_decodeValue(value){
+  if (decodeURIComponent) {
+    return decodeURIComponent(value);
+  } 
+  if(unescape){
+  	return unescape(value);
+  }
+}
+
+/** Brower-dependent functions to find window inner size, and scroll status */
+function f_clientWidth() {
+	return f_filterResults (
+		window.innerWidth ? window.innerWidth : 0,
+		document.documentElement ? document.documentElement.clientWidth : 0,
+		document.body ? document.body.clientWidth : 0
+	);
+}
+function f_clientHeight() {
+	return f_filterResults (
+		window.innerHeight ? window.innerHeight : 0,
+		document.documentElement ? document.documentElement.clientHeight : 0,
+		document.body ? document.body.clientHeight : 0
+	);
+}
+function f_scrollLeft() {
+	return f_filterResults (
+		window.pageXOffset ? window.pageXOffset : 0,
+		document.documentElement ? document.documentElement.scrollLeft : 0,
+		document.body ? document.body.scrollLeft : 0
+	);
+}
+function f_scrollTop() {
+	return f_filterResults (
+		window.pageYOffset ? window.pageYOffset : 0,
+		document.documentElement ? document.documentElement.scrollTop : 0,
+		document.body ? document.body.scrollTop : 0
+	);
+}
+function f_filterResults(n_win, n_docel, n_body) {
+	var n_result = n_win ? n_win : 0;
+	if (n_docel && (!n_result || (n_result > n_docel)))
+		n_result = n_docel;
+	return n_body && (!n_result || (n_result > n_body)) ? n_body : n_result;
+}
+
+/** Get the height available for the results container */
+function os_availableHeight(r){
+	var absTop = document.getElementById(r.container).style.top;
+	var px = absTop.lastIndexOf("px");
+	if(px > 0)
+		absTop = absTop.substring(0,px);
+	return f_clientHeight() - (absTop - f_scrollTop());
+}
+
+
+/** Get element absolute position {left,top} */
+function os_getElementPosition(elemID){
+	var offsetTrail = document.getElementById(elemID);
+	var offsetLeft = 0;
+	var offsetTop = 0;
+	while (offsetTrail){
+		offsetLeft += offsetTrail.offsetLeft;
+		offsetTop += offsetTrail.offsetTop;
+		offsetTrail = offsetTrail.offsetParent;
+	}
+	if (navigator.userAgent.indexOf('Mac') != -1 && typeof document.body.leftMargin != 'undefined'){
+		offsetLeft += document.body.leftMargin;
+		offsetTop += document.body.topMargin;
+	}
+	return {left:offsetLeft,top:offsetTop};
+}
+
+/** Create the container div that will hold the suggested titles */
+function os_createContainer(r){
+	var c = document.createElement("div");
+	var s = document.getElementById(r.searchbox);
+	var pos = os_getElementPosition(r.searchbox);	
+	var left = pos.left;
+	var top = pos.top + s.offsetHeight;
+	var body = document.getElementById("globalWrapper");
+	c.className = "os-suggest";
+	c.setAttribute("id", r.container);	
+	body.appendChild(c); 
+	
+	// dynamically generated style params	
+	// IE workaround, cannot explicitely set "style" attribute
+	c = document.getElementById(r.container);
+	c.style.top = top+"px";
+	c.style.left = left+"px";
+	c.style.width = s.offsetWidth+"px";
+	
+	// mouse event handlers
+	c.onmouseover = function(event) { os_eventMouseover(r.searchbox, event); };
+	c.onmousemove = function(event) { os_eventMousemove(r.searchbox, event); };
+	c.onmousedown = function(event) { return os_eventMousedown(r.searchbox, event); };
+	c.onmouseup = function(event) { os_eventMouseup(r.searchbox, event); };
+	return c;
+}
+
+/** change container height to fit to screen */
+function os_fitContainer(r){	
+	var c = document.getElementById(r.container);
+	var h = os_availableHeight(r) - 20;
+	var inc = r.containerRow;
+	h = parseInt(h/inc) * inc;
+	if(h < (2 * inc) && r.resultCount > 1) // min: two results
+		h = 2 * inc;	
+	if((h/inc) > os_max_lines_per_suggest )
+		h = inc * os_max_lines_per_suggest;
+	if(h < r.containerTotal){
+		c.style.height = h +"px";
+		r.containerCount = parseInt(Math.round(h/inc));
+	} else{
+		c.style.height = r.containerTotal+"px";
+		r.containerCount = r.resultCount;
+	}
+}
+/** If some entries are longer than the box, replace text with "..." */
+function os_trimResultText(r){
+	var w = document.getElementById(r.container).offsetWidth;
+	if(r.containerCount < r.resultCount){		
+		w -= 20; // give 20px for scrollbar		
+	} else
+		w = os_operaWidthFix(w);
+	if(w < 10)
+		return;
+	for(var i=0;i<r.resultCount;i++){
+		var e = document.getElementById(r.resultText+i);
+		var replace = 1;
+		var lastW = e.offsetWidth+1;
+		var iteration = 0;
+		var changedText = false;
+		while(e.offsetWidth > w && (e.offsetWidth < lastW || iteration<2)){
+			changedText = true;
+			lastW = e.offsetWidth;
+			var l = e.innerHTML;			
+			e.innerHTML = l.substring(0,l.length-replace)+"...";
+			iteration++;
+			replace = 4; // how many chars to replace
+		}
+		if(changedText){
+			// show hint for trimmed titles
+			document.getElementById(r.resultTable+i).setAttribute("title",r.results[i]);
+		}
+	}
+}
+
+/** Handles data from XMLHttpRequest, and updates the suggest results */
+function os_updateResults(r, query, text, cacheKey){	 
+	os_cache[cacheKey] = text;
+	r.query = query;
+	r.original = query;
+	if(text == ""){
+		r.results = null;
+		r.resultCount = 0;
+		os_hideResults(r);
+	} else{		
+		try {
+			var p = eval('('+text+')'); // simple json parse, could do a safer one
+			if(p.length<2 || p[1].length == 0){
+				r.results = null;
+				r.resultCount = 0;
+				os_hideResults(r);
+				return;
+			}		
+			var c = document.getElementById(r.container);
+			if(c == null)
+				c = os_createContainer(r);			
+			c.innerHTML = os_createResultTable(r,p[1]);
+			// init container table sizes
+			var t = document.getElementById(r.resultTable);		
+			r.containerTotal = t.offsetHeight;	
+			r.containerRow = t.offsetHeight / r.resultCount;
+			os_trimResultText(r);				
+			os_showResults(r);
+		} catch(e){
+			// bad response from server or such
+			os_hideResults(r);			
+			os_cache[cacheKey] = null;
+		}
+	}	
+}
+
+/** Create the result table to be placed in the container div */
+function os_createResultTable(r, results){
+	var c = document.getElementById(r.container);
+	var width = os_operaWidthFix(c.offsetWidth);	
+	var html = "<table class=\"os-suggest-results\" id=\""+r.resultTable+"\" style=\"width: "+width+"px;\">";
+	r.results = new Array();
+	r.resultCount = results.length;
+	for(i=0;i<results.length;i++){
+		var title = os_decodeValue(results[i]);
+		r.results[i] = title;
+		html += "<tr><td class=\"os-suggest-result\" id=\""+r.resultTable+i+"\"><span id=\""+r.resultText+i+"\">"+title+"</span></td></tr>";
+	}
+	html+="</table>"
+	return html;
+}
+
+/** Fetch namespaces from checkboxes or hidden fields in the search form,
+    if none defined use wgSearchNamespaces global */
+function os_getNamespaces(r){	
+	var namespaces = "";
+	var elements = document.forms[r.searchform].elements;
+	for(i=0; i < elements.length; i++){
+		var name = elements[i].name;
+		if(typeof name != 'undefined' && name.length > 2 
+		&& name[0]=='n' && name[1]=='s' 
+		&& ((elements[i].type=='checkbox' && elements[i].checked) 
+		 	|| (elements[i].type=='hidden' && elements[i].value=="1")) ){
+			if(namespaces!="")
+				namespaces+="|";
+			namespaces+=name.substring(2);
+		}
+	}
+	if(namespaces == "")
+		namespaces = wgSearchNamespaces.join("|");
+	return namespaces;
+}
+
+/** Update results if user hasn't already typed something else */
+function os_updateIfRelevant(r, query, text, cacheKey){
+	var t = document.getElementById(r.searchbox);
+	if(t != null && t.value == query){ // check if response is still relevant	        			
+		os_updateResults(r, query, text, cacheKey);
+	}
+	r.query = query;
+}
+
+/** Fetch results after some timeout */
+function os_delayedFetch(){
+	if(os_timer == null)
+		return;
+	var r = os_timer.r;
+	var query = os_timer.query;
+	os_timer = null;
+	var path = wgMWSuggestTemplate.replace("{namespaces}",os_getNamespaces(r))
+							  	  .replace("{dbname}",wgDBname)
+							  	  .replace("{searchTerms}",os_encodeQuery(query));
+	
+	// try to get from cache, if not fetch using ajax
+	var cached = os_cache[path];
+	if(cached != null){
+		os_updateIfRelevant(r, query, cached, path);
+	} else{									  
+		var xmlhttp = sajax_init_object();
+		if(xmlhttp){
+			try {			
+				xmlhttp.open("GET", path, true);
+				xmlhttp.onreadystatechange=function(){
+		        	if (xmlhttp.readyState==4 && typeof os_updateIfRelevant == 'function') {	        		
+		        		os_updateIfRelevant(r, query, xmlhttp.responseText, path);
+	        		}
+	      		};
+	     		xmlhttp.send(null);     	
+	     	} catch (e) {
+				if (window.location.hostname == "localhost") {
+					alert("Your browser blocks XMLHttpRequest to 'localhost', try using a real hostname for development/testing.");
+				}
+				throw e;
+			}
+		}
+	}
+}
+
+/** Init timed update via os_delayedUpdate() */
+function os_fetchResults(r, query, timeout){
+	if(query == ""){
+		os_hideResults(r);
+		return;
+	} else if(query == r.query)
+		return; // no change
+	
+	os_is_stopped = false; // make sure we're running
+	
+	/* var cacheKey = wgDBname+":"+query; 
+	var cached = os_cache[cacheKey];
+	if(cached != null){
+		os_updateResults(r,wgDBname,query,cached);
+		return;
+	} */
+	
+	// cancel any pending fetches
+	if(os_timer != null && os_timer.id != null)
+		clearTimeout(os_timer.id);
+	// schedule delayed fetching of results	
+	if(timeout != 0){
+		os_timer = new os_Timer(setTimeout("os_delayedFetch()",timeout),r,query);
+	} else{		
+		os_timer = new os_Timer(null,r,query);
+		os_delayedFetch(); // do it now!
+	}
+
+}
+/** Change the highlighted row (i.e. suggestion), from position cur to next */
+function os_changeHighlight(r, cur, next, updateSearchBox){
+	if (next >= r.resultCount)
+		next = r.resultCount-1;
+	if (next < -1)
+		next = -1;   
+	r.selected = next;
+   	if (cur == next)
+    	return; // nothing to do.
+    
+    if(cur >= 0){
+    	var curRow = document.getElementById(r.resultTable + cur);
+    	if(curRow != null)
+    		curRow.className = "os-suggest-result";
+    }
+    var newText;
+    if(next >= 0){
+    	var nextRow = document.getElementById(r.resultTable + next);
+    	if(nextRow != null)
+    		nextRow.className = "os-suggest-result-hl";
+    	newText = r.results[next];
+    } else
+    	newText = r.original;
+    	
+    // adjust the scrollbar if any
+    if(r.containerCount < r.resultCount){
+    	var c = document.getElementById(r.container);
+    	var vStart = c.scrollTop / r.containerRow;
+    	var vEnd = vStart + r.containerCount;
+    	if(next < vStart)
+    		c.scrollTop = next * r.containerRow;
+    	else if(next >= vEnd)
+    		c.scrollTop = (next - r.containerCount + 1) * r.containerRow;
+    }
+    	
+    // update the contents of the search box
+    if(updateSearchBox){
+    	os_updateSearchQuery(r,newText);	
+    }
+}
+
+function os_updateSearchQuery(r,newText){
+	document.getElementById(r.searchbox).value = newText;
+    r.query = newText;
+}
+
+/** Find event target */
+function os_getTarget(e){
+	if (!e) var e = window.event;
+	if (e.target) return e.target;
+	else if (e.srcElement) return e.srcElement;
+	else return null;
+}
+
+
+
+/********************
+ *  Keyboard events 
+ ********************/ 
+
+/** Event handler that will fetch results on keyup */
+function os_eventKeyup(e){
+	var targ = os_getTarget(e);
+	var r = os_map[targ.id];
+	if(r == null)
+		return; // not our event
+		
+	// some browsers won't generate keypressed for arrow keys, catch it 
+	if(os_keypressed_count == 0){
+		os_processKey(r,os_cur_keypressed,targ);
+	}
+	var query = targ.value;
+	os_fetchResults(r,query,os_search_timeout);
+}
+
+/** catch arrows up/down and escape to hide the suggestions */
+function os_processKey(r,keypressed,targ){
+	if (keypressed == 40){ // Arrow Down
+    	if (r.visible) {      		
+      		os_changeHighlight(r, r.selected, r.selected+1, true);      		
+    	} else if(os_timer == null){
+    		// user wants to get suggestions now
+    		r.query = "";
+			os_fetchResults(r,targ.value,0);
+    	}
+  	} else if (keypressed == 38){ // Arrow Up
+  		if (r.visible){
+  			os_changeHighlight(r, r.selected, r.selected-1, true);
+  		}
+  	} else if(keypressed == 27){ // Escape
+  		document.getElementById(r.searchbox).value = r.original;
+  		r.query = r.original;
+  		os_hideResults(r);
+  	} else if(r.query != document.getElementById(r.searchbox).value){
+  		// os_hideResults(r); // don't show old suggestions
+  	}
+}
+
+/** When keys is held down use a timer to output regular events */
+function os_eventKeypress(e){	
+	var targ = os_getTarget(e);
+	var r = os_map[targ.id];
+	if(r == null)
+		return; // not our event
+	
+	var keypressed = os_cur_keypressed;
+	if(keypressed == 38 || keypressed == 40){
+		var d = new Date()
+		var now = d.getTime();
+		if(now - os_last_keypress < 120){
+			os_last_keypress = now;
+			return;
+		}
+	}
+	
+	os_keypressed_count++;
+	os_processKey(r,keypressed,targ);
+}
+
+/** Catch the key code (Firefox bug)  */
+function os_eventKeydown(e){
+	var targ = os_getTarget(e);
+	var r = os_map[targ.id];
+	if(r == null)
+		return; // not our event
+		
+	os_mouse_moved = false;
+		
+	if(os_first_focus){
+		// firefox bug, focus&defocus to make autocomplete=off valid
+		targ.blur(); targ.focus();
+		os_first_focus = false;
+	}
+
+	os_cur_keypressed = (window.Event) ? e.which : e.keyCode;
+	os_last_keypress = 0;
+	os_keypressed_count = 0;
+}
+
+/** Event: loss of focus of input box */
+function os_eventBlur(e){	
+	if(os_first_focus)
+		return; // we are focusing/defocusing
+	var targ = os_getTarget(e);
+	var r = os_map[targ.id];
+	if(r == null)
+		return; // not our event
+	if(!os_mouse_pressed)	
+		os_hideResults(r);
+}
+
+/** Event: focus (catch only when stopped) */
+function os_eventFocus(e){	
+	if(os_first_focus)
+		return; // we are focusing/defocusing
+}
+
+
+
+/********************
+ *  Mouse events 
+ ********************/ 
+
+/** Mouse over the container */
+function os_eventMouseover(srcId, e){
+	var targ = os_getTarget(e);	
+	var r = os_map[srcId];
+	if(r == null || !os_mouse_moved)
+		return; // not our event
+	var num = os_getNumberSuffix(targ.id);
+	if(num >= 0)
+		os_changeHighlight(r,r.selected,num,false);
+					
+}
+
+/* Get row where the event occured (from its id) */
+function os_getNumberSuffix(id){
+	var num = id.substring(id.length-2);
+	if( ! (num.charAt(0) >= '0' && num.charAt(0) <= '9') )
+		num = num.substring(1);
+	if(os_isNumber(num))
+		return parseInt(num);
+	else
+		return -1;
+}
+
+/** Save mouse move as last action */
+function os_eventMousemove(srcId, e){
+	os_mouse_moved = true;
+}
+
+/** Mouse button held down, register possible click  */
+function os_eventMousedown(srcId, e){
+	var targ = os_getTarget(e);
+	var r = os_map[srcId];
+	if(r == null)
+		return; // not our event
+	var num = os_getNumberSuffix(targ.id);
+	
+	os_mouse_pressed = true;
+	if(num >= 0){
+		os_mouse_num = num;
+		// os_updateSearchQuery(r,r.results[num]);
+	}
+	// keep the focus on the search field
+	document.getElementById(r.searchbox).focus();
+	
+	return false; // prevents selection
+}
+
+/** Mouse button released, check for click on some row */
+function os_eventMouseup(srcId, e){
+	var targ = os_getTarget(e);
+	var r = os_map[srcId];
+	if(r == null)
+		return; // not our event
+	var num = os_getNumberSuffix(targ.id);
+		
+	if(num >= 0 && os_mouse_num == num){
+		os_updateSearchQuery(r,r.results[num]);
+		os_hideResults(r);
+		document.getElementById(r.searchform).submit();
+	}
+	os_mouse_pressed = false;
+	// keep the focus on the search field
+	document.getElementById(r.searchbox).focus();
+}
+
+/** Check if x is a valid integer */
+function os_isNumber(x){
+	if(x == "" || isNaN(x))
+		return false;
+	for(var i=0;i<x.length;i++){
+		var c = x.charAt(i);
+		if( ! (c >= '0' && c <= '9') )
+			return false;
+	}
+	return true;
+}
+
+
+/** When the form is submitted hide everything, cancel updates... */
+function os_eventOnsubmit(e){
+	var targ = os_getTarget(e);
+
+	os_is_stopped = true;
+	// kill timed requests
+	if(os_timer != null && os_timer.id != null){
+		clearTimeout(os_timer.id);
+		os_timer = null;
+	}
+	// Hide all suggestions
+	for(i=0;i<os_autoload_inputs.length;i++){
+		var r = os_map[os_autoload_inputs[i]];
+		if(r != null){
+			var b = document.getElementById(r.searchform);
+			if(b != null && b == targ){ 
+				// set query value so the handler won't try to fetch additional results
+				r.query = document.getElementById(r.searchbox).value;
+			}			
+			os_hideResults(r);
+		}
+	}
+	return true;
+}
+
+/** Init Result objects and event handlers */
+function os_initHandlers(name, formname, element){
+	var r = new os_Results(name, formname);	
+	// event handler
+	element.onkeyup = function(event) { os_eventKeyup(event); };
+	element.onkeydown = function(event) { os_eventKeydown(event); };
+	element.onkeypress = function(event) { os_eventKeypress(event); };
+	element.onblur = function(event) { os_eventBlur(event); };
+	element.onfocus = function(event) { os_eventFocus(event); };
+	element.setAttribute("autocomplete","off");
+	// stopping handler
+	document.getElementById(formname).onsubmit = function(event){ return os_eventOnsubmit(event); };
+	os_map[name] = r; 
+	// toggle link
+	if(document.getElementById(r.toggle) == null){
+		// TODO: disable this while we figure out a way for this to work in all browsers 
+		/* if(name=='searchInput'){
+			// special case: place above the main search box
+			var t = os_createToggle(r,"os-suggest-toggle");
+			var searchBody = document.getElementById('searchBody');
+			var first = searchBody.parentNode.firstChild.nextSibling.appendChild(t);
+		} else{
+			// default: place below search box to the right
+			var t = os_createToggle(r,"os-suggest-toggle-def");
+			var top = element.offsetTop + element.offsetHeight;
+			var left = element.offsetLeft + element.offsetWidth;
+			t.style.position = "absolute";
+			t.style.top = top + "px";
+			t.style.left = left + "px";
+			element.parentNode.appendChild(t);
+			// only now width gets calculated, shift right
+			left -= t.offsetWidth;
+			t.style.left = left + "px";
+			t.style.visibility = "visible";
+		} */
+	}
+	
+}
+
+/** Return the span element that contains the toggle link */
+function os_createToggle(r,className){
+	var t = document.createElement("span");
+	t.className = className;
+	t.setAttribute("id", r.toggle);
+	var link = document.createElement("a");
+	link.setAttribute("href","javascript:void(0);");
+	link.onclick = function(){ os_toggle(r.searchbox,r.searchform) };
+	var msg = document.createTextNode(wgMWSuggestMessages[0]);
+	link.appendChild(msg);
+	t.appendChild(link);
+	return t; 	
+}
+
+/** Call when user clicks on some of the toggle links */
+function os_toggle(inputId,formName){
+	r = os_map[inputId];
+	var msg = '';
+	if(r == null){
+		os_enableSuggestionsOn(inputId,formName);
+		r = os_map[inputId];
+		msg = wgMWSuggestMessages[0];		
+	} else{
+		os_disableSuggestionsOn(inputId,formName);
+		msg = wgMWSuggestMessages[1];
+	}
+	// change message
+	var link = document.getElementById(r.toggle).firstChild;
+	link.replaceChild(document.createTextNode(msg),link.firstChild);
+}
+
+/** Call this to enable suggestions on input (id=inputId), on a form (name=formName) */
+function os_enableSuggestionsOn(inputId, formName){
+	os_initHandlers( inputId, formName, document.getElementById(inputId) );
+}
+
+/** Call this to disable suggestios on input box (id=inputId) */
+function os_disableSuggestionsOn(inputId){
+	r = os_map[inputId];
+	if(r != null){
+		// cancel/hide results
+		os_timer = null;
+		os_hideResults(r);
+		// turn autocomplete on !
+		document.getElementById(inputId).setAttribute("autocomplete","on");
+		// remove descriptor	
+		os_map[inputId] = null;
+	}
+}
+
+/** Initialization, call upon page onload */
+function os_MWSuggestInit() {
+	for(i=0;i<os_autoload_inputs.length;i++){
+		var id = os_autoload_inputs[i];
+		var form = os_autoload_forms[i];
+		element = document.getElementById( id );
+		if(element != null)
+			os_initHandlers(id,form,element);
+	}	
+}
+
+hookEvent("load", os_MWSuggestInit);
Index: trunk/phase3/docs/hooks.txt
===================================================================
--- trunk/phase3/docs/hooks.txt	(revision 33399)
+++ trunk/phase3/docs/hooks.txt	(revision 33400)
@@ -886,7 +886,7 @@
 
 'PrefixSearchBackend': Override the title prefix search used for OpenSearch and
 AJAX search suggestions. Put results into &$results outparam and return false.
-$ns : int namespace key to search in
+$ns : array of int namespace keys to search in
 $search : search term (not guaranteed to be conveniently normalized)
 $limit : maximum number of results to return
 &$results : out param: array of page names (strings)
Index: trunk/phase3/includes/PrefixSearch.php
===================================================================
--- trunk/phase3/includes/PrefixSearch.php	(revision 33399)
+++ trunk/phase3/includes/PrefixSearch.php	(revision 33400)
@@ -5,19 +5,23 @@
 	 * Do a prefix search of titles and return a list of matching page names.
 	 * @param string $search
 	 * @param int $limit
+	 * @param array $namespaces - used if query is not explicitely prefixed
 	 * @return array of strings
 	 */
-	public static function titleSearch( $search, $limit ) {
+	public static function titleSearch( $search, $limit, $namespaces=array() ) {
 		$search = trim( $search );
 		if( $search == '' ) {
 			return array(); // Return empty result
 		}
-
+		$namespaces = self::validateNamespaces( $namespaces );
+		
 		$title = Title::newFromText( $search );
 		if( $title && $title->getInterwiki() == '' ) {
-			$ns = $title->getNamespace();
+			$ns = array($title->getNamespace());
+			if($ns[0] == NS_MAIN) 
+				$ns = $namespaces; // no explicit prefix, use default namespaces
 			return self::searchBackend(
-				$title->getNamespace(), $title->getText(), $limit );
+				$ns, $title->getText(), $limit );
 		}
 
 		// Is this a namespace prefix?
@@ -26,29 +30,32 @@
 			&& $title->getNamespace() != NS_MAIN
 			&& $title->getInterwiki() == '' ) {
 			return self::searchBackend(
-				$title->getNamespace(), '', $limit );
+				array($title->getNamespace()), '', $limit );
 		}
-
-		return self::searchBackend( 0, $search, $limit );
+				
+		return self::searchBackend( $namespaces, $search, $limit );
 	}
 
 
 	/**
 	 * Do a prefix search of titles and return a list of matching page names.
+	 * @param array $namespaces
 	 * @param string $search
 	 * @param int $limit
 	 * @return array of strings
 	 */
-	protected static function searchBackend( $ns, $search, $limit ) {
-		if( $ns == NS_MEDIA ) {
-			$ns = NS_IMAGE;
-		} elseif( $ns == NS_SPECIAL ) {
-			return self::specialSearch( $search, $limit );
+	protected static function searchBackend( $namespaces, $search, $limit ) {
+		if( count($namespaces) == 1 ){
+			$ns = $namespaces[0];
+			if( $ns == NS_MEDIA ) {
+				$namespaces = array(NS_IMAGE);
+			} elseif( $ns == NS_SPECIAL ) {
+				return self::specialSearch( $search, $limit );
+			}
 		}
-
 		$srchres = array();
-		if( wfRunHooks( 'PrefixSearchBackend', array( $ns, $search, $limit, &$srchres ) ) ) {
-			return self::defaultSearchBackend( $ns, $search, $limit );
+		if( wfRunHooks( 'PrefixSearchBackend', array( $namespaces, $search, $limit, &$srchres ) ) ) {
+			return self::defaultSearchBackend( $namespaces, $search, $limit );
 		}
 		return $srchres;
 	}
@@ -91,18 +98,22 @@
 	 * Unless overridden by PrefixSearchBackend hook...
 	 * This is case-sensitive except the first letter (per $wgCapitalLinks)
 	 *
-	 * @param int $ns Namespace to search in
+	 * @param array $namespaces Namespaces to search in
 	 * @param string $search term
 	 * @param int $limit max number of items to return
 	 * @return array of title strings
 	 */
-	protected static function defaultSearchBackend( $ns, $search, $limit ) {
+	protected static function defaultSearchBackend( $namespaces, $search, $limit ) {
 		global $wgCapitalLinks, $wgContLang;
 
 		if( $wgCapitalLinks ) {
 			$search = $wgContLang->ucfirst( $search );
 		}
 
+		$ns = array_shift($namespaces); // support only one namespace
+		if( in_array(NS_MAIN,$namespaces))
+			$ns = NS_MAIN; // if searching on many always default to main 
+		
 		// Prepare nested request
 		$req = new FauxRequest(array (
 			'action' => 'query',
@@ -129,5 +140,25 @@
 
 		return $srchres;
 	}
-
+	
+	/**
+	 * Validate an array of numerical namespace indexes
+	 * 
+	 * @param array $namespaces
+	 */
+	protected static function validateNamespaces($namespaces){
+		global $wgContLang;
+		$validNamespaces = $wgContLang->getNamespaces();
+		if( is_array($namespaces) && count($namespaces)>0 ){
+			$valid = array();
+			foreach ($namespaces as $ns){
+				if( is_numeric($ns) && array_key_exists($ns, $validNamespaces) )
+					$valid[] = $ns;
+			}
+			if( count($valid) > 0 )
+				return $valid;
+		}
+		
+		return array( NS_MAIN );
+	}
 }
Index: trunk/phase3/includes/SearchEngine.php
===================================================================
--- trunk/phase3/includes/SearchEngine.php	(revision 33399)
+++ trunk/phase3/includes/SearchEngine.php	(revision 33400)
@@ -35,7 +35,7 @@
 	function searchTitle( $term ) {
 		return null;
 	}
-
+	
 	/**
 	 * If an exact title match can be find, or a very slightly close match,
 	 * return the title. If no match, returns NULL.
@@ -222,6 +222,50 @@
 		}
 		return $arr;
 	}
+	
+	/**
+	 * Extract default namespaces to search from the given user's
+	 * settings, returning a list of index numbers.
+	 *
+	 * @param User $user
+	 * @return array
+	 * @static 
+	 */
+	public static function userNamespaces( &$user ) {
+		$arr = array();
+		foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+			if( $user->getOption( 'searchNs' . $ns ) ) {
+				$arr[] = $ns;
+			}
+		}
+		return $arr;
+	}
+	
+	/**
+	 * Find snippet highlight settings for a given user
+	 *
+	 * @param User $user
+	 * @return array contextlines, contextchars 
+	 * @static
+	 */
+	public static function userHighlightPrefs( &$user ){
+		//$contextlines = $user->getOption( 'contextlines',  5 );
+		$contextlines = 2; // Hardcode this. Old defaults sucked. :)
+		$contextchars = $user->getOption( 'contextchars', 50 );
+		return array($contextlines, $contextchars);
+	}
+	
+	/**
+	 * An array of namespaces indexes to be searched by default
+	 * 
+	 * @return array 
+	 * @static
+	 */
+	public static function defaultNamespaces(){
+		global $wgNamespacesToBeSearchedDefault;
+		
+		return array_keys($wgNamespacesToBeSearchedDefault, true);
+	}
 
 	/**
 	 * Return a 'cleaned up' search string
@@ -281,6 +325,37 @@
 	function updateTitle( $id, $title ) {
 		// no-op
 	}
+	
+	/**
+	 * Get OpenSearch suggestion template
+	 * 
+	 * @return string
+	 * @static 
+	 */
+	public static function getOpenSearchTemplate() {
+		global $wgOpenSearchTemplate, $wgServer, $wgScriptPath;
+		if($wgOpenSearchTemplate)		
+			return $wgOpenSearchTemplate;
+		else{ 
+			$ns = implode(',',SearchEngine::defaultNamespaces());
+			if(!$ns) $ns = "0";
+			return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace='.$ns;
+		}
+	}
+	
+	/**
+	 * Get internal MediaWiki Suggest template 
+	 * 
+	 * @return string
+	 * @static
+	 */
+	public static function getMWSuggestTemplate() {
+		global $wgMWSuggestTemplate, $wgServer, $wgScriptPath;
+		if($wgMWSuggestTemplate)		
+			return $wgMWSuggestTemplate;
+		else 
+			return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace={namespaces}';
+	}
 }
 
 
@@ -352,6 +427,35 @@
 	function getSuggestionSnippet(){
 		return '';
 	}
+	
+	/**
+	 * Return information about how and from where the results were fetched,
+	 * should be useful for diagnostics and debugging 
+	 *
+	 * @return string
+	 */
+	function getInfo() {
+		return null;
+	}
+	
+	/**
+	 * Return a result set of hits on other (multiple) wikis associated with this one
+	 *
+	 * @return SearchResultSet
+	 */
+	function getInterwikiResults() {
+		return null;
+	}
+	
+	/**
+	 * Check if there are results on other wikis
+	 *
+	 * @return boolean
+	 */
+	function hasInterwikiResults() {
+		return $this->getInterwikiResults() != null;
+	}
+	
 
 	/**
 	 * Fetches next search result, or false.
@@ -388,7 +492,33 @@
 
 	function SearchResult( $row ) {
 		$this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
+		if( !is_null($this->mTitle) )
+			$this->mRevision = Revision::newFromTitle( $this->mTitle );
 	}
+	
+	/**
+	 * Check if this is result points to an invalid title
+	 *
+	 * @return boolean
+	 * @access public
+	 */
+	function isBrokenTitle(){
+		if( is_null($this->mTitle) )
+			return true;
+		return false;
+	}
+	
+	/**
+	 * Check if target page is missing, happens when index is out of date
+	 * 
+	 * @return boolean
+	 * @access public
+	 */
+	function isMissingRevision(){
+		if( !$this->mRevision )
+			return true;
+		return false;
+	}
 
 	/**
 	 * @return Title
@@ -406,23 +536,93 @@
 	}
 
 	/**
-	 * @return string highlighted text snippet, null if not supported
+	 * Lazy initialization of article text from DB
 	 */
-	function getTextSnippet(){
-		return null;
+	protected function initText(){
+		if( !isset($this->mText) ){
+			$this->mText = $this->mRevision->getText();
+		}
 	}
 
 	/**
+	 * @param array $terms terms to highlight
+	 * @return string highlighted text snippet, null (and not '') if not supported 
+	 */
+	function getTextSnippet($terms){
+		global $wgUser;
+		$this->initText();
+		list($contextlines,$contextchars) = SearchEngine::userHighlightPrefs($wgUser);
+		return $this->extractText( $this->mText, $terms, $contextlines, $contextchars); 		
+	}
+	
+	/**
+	 * Default implementation of snippet extraction
+	 *
+	 * @param string $text
+	 * @param array $terms
+	 * @param int $contextlines
+	 * @param int $contextchars
+	 * @return string
+	 */
+	protected function extractText( $text, $terms, $contextlines, $contextchars ) {
+		global $wgLang, $wgContLang;
+		$fname = __METHOD__;
+	
+		$lines = explode( "\n", $text );
+		
+		$terms = implode( '|', $terms );
+		$max = intval( $contextchars ) + 1;
+		$pat1 = "/(.*)($terms)(.{0,$max})/i";
+
+		$lineno = 0;
+
+		$extract = "";
+		wfProfileIn( "$fname-extract" );
+		foreach ( $lines as $line ) {
+			if ( 0 == $contextlines ) {
+				break;
+			}
+			++$lineno;
+			$m = array();
+			if ( ! preg_match( $pat1, $line, $m ) ) {
+				continue;
+			}
+			--$contextlines;
+			$pre = $wgContLang->truncate( $m[1], -$contextchars, ' ... ' );
+
+			if ( count( $m ) < 3 ) {
+				$post = '';
+			} else {
+				$post = $wgContLang->truncate( $m[3], $contextchars, ' ... ' );
+			}
+
+			$found = $m[2];
+
+			$line = htmlspecialchars( $pre . $found . $post );
+			$pat2 = '/(' . $terms . ")/i";
+			$line = preg_replace( $pat2,
+			  "<span class='searchmatch'>\\1</span>", $line );
+
+			$extract .= "${line}\n";
+		}
+		wfProfileOut( "$fname-extract" );
+		
+		return $extract;
+	}
+	
+	/**
+	 * @param array $terms terms to highlight
 	 * @return string highlighted title, '' if not supported
 	 */
-	function getTitleSnippet(){
+	function getTitleSnippet($terms){
 		return '';
 	}
 
 	/**
+	 * @param array $terms terms to highlight
 	 * @return string highlighted redirect name (redirect to this page), '' if none or not supported
 	 */
-	function getRedirectSnippet(){
+	function getRedirectSnippet($terms){
 		return '';
 	}
 
@@ -448,25 +648,41 @@
 	}
 
 	/**
-	 * @return string timestamp, null if not supported
+	 * @return string timestamp
 	 */
 	function getTimestamp(){
-		return null;
+		return $this->mRevision->getTimestamp();
 	}
 
 	/**
-	 * @return int number of words, null if not supported
+	 * @return int number of words
 	 */
 	function getWordCount(){
-		return null;
+		$this->initText();
+		return str_word_count( $this->mText );
 	}
 
 	/**
-	 * @return int size in bytes, null if not supported
+	 * @return int size in bytes
 	 */
 	function getByteSize(){
-		return null;
+		$this->initText();
+		return strlen( $this->mText );
 	}
+	
+	/**
+	 * @return boolean if hit has related articles
+	 */
+	function hasRelated(){
+		return false;
+	}
+	
+	/**
+	 * @return interwiki prefix of the title (return iw even if title is broken)
+	 */
+	function getInterwikiPrefix(){
+		return '';
+	}
 }
 
 /**
Index: trunk/phase3/includes/OutputPage.php
===================================================================
--- trunk/phase3/includes/OutputPage.php	(revision 33399)
+++ trunk/phase3/includes/OutputPage.php	(revision 33400)
@@ -670,7 +670,7 @@
 		global $wgUser, $wgOutputEncoding, $wgRequest;
 		global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType;
 		global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgAjaxSearch, $wgAjaxWatch;
-		global $wgServer, $wgStyleVersion;
+		global $wgServer, $wgStyleVersion, $wgEnableMWSuggest;
 
 		if( $this->mDoNothing ){
 			return;
@@ -772,10 +772,13 @@
 			if( $wgAjaxWatch && $wgUser->isLoggedIn() ) {
 				$this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxwatch.js?$wgStyleVersion\"></script>\n" );
 			}
+			
+			if ( $wgEnableMWSuggest && !$wgUser->getOption( 'disablesuggest', false ) ){
+				$this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/mwsuggest.js?$wgStyleVersion\"></script>\n" );					
+			}
 		}
 
 
-
 		# Buffer output; final headers may depend on later processing
 		ob_start();
 
Index: trunk/phase3/includes/api/ApiOpenSearch.php
===================================================================
--- trunk/phase3/includes/api/ApiOpenSearch.php	(revision 33399)
+++ trunk/phase3/includes/api/ApiOpenSearch.php	(revision 33400)
@@ -45,11 +45,12 @@
 		$params = $this->extractRequestParams();
 		$search = $params['search'];
 		$limit = $params['limit'];
-
+		$namespaces = $params['namespace'];
+		
 		// Open search results may be stored for a very long time
 		$this->getMain()->setCacheMaxAge(1200);
 
-		$srchres = PrefixSearch::titleSearch( $search, $limit );
+		$srchres = PrefixSearch::titleSearch( $search, $limit, $namespaces );
 
 		// Set top level elements
 		$result = $this->getResult();
@@ -66,14 +67,20 @@
 				ApiBase :: PARAM_MIN => 1,
 				ApiBase :: PARAM_MAX => 100,
 				ApiBase :: PARAM_MAX2 => 100
-			)
+			),
+			'namespace' => array(
+				ApiBase :: PARAM_DFLT => NS_MAIN,
+				ApiBase :: PARAM_TYPE => 'namespace',
+				ApiBase :: PARAM_ISMULTI => true
+			),
 		);
 	}
 
 	public function getParamDescription() {
 		return array (
 			'search' => 'Search string',
-			'limit' => 'Maximum amount of results to return'
+			'limit' => 'Maximum amount of results to return',
+			'namespace' => 'Namespaces to search',
 		);
 	}
 
Index: trunk/phase3/includes/SpecialPreferences.php
===================================================================
--- trunk/phase3/includes/SpecialPreferences.php	(revision 33399)
+++ trunk/phase3/includes/SpecialPreferences.php	(revision 33400)
@@ -66,6 +66,7 @@
 		$this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' );
 		$this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' );
 		$this->mUseAjaxSearch = $request->getCheck( 'wpUseAjaxSearch' );
+		$this->mDisableMWSuggest = $request->getCheck( 'wpDisableMWSuggest' );
 
 		$this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) &&
 			$this->mPosted &&
@@ -288,6 +289,7 @@
 		$wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) );
 		$wgUser->setOption( 'watchlistdays', $this->validateFloat( $this->mWatchlistDays, 0, 7 ) );
 		$wgUser->setOption( 'ajaxsearch', $this->mUseAjaxSearch );
+		$wgUser->setOption( 'disablesuggest', $this->mDisableMWSuggest );
 
 		# Set search namespace options
 		foreach( $this->mSearchNs as $i => $value ) {
@@ -400,6 +402,7 @@
 		$this->mUnderline = $wgUser->getOption( 'underline' );
 		$this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' );
 		$this->mUseAjaxSearch = $wgUser->getBoolOption( 'ajaxsearch' );
+		$this->mDisableMWSuggest = $wgUser->getBoolOption( 'disablesuggest' );
 
 		$togs = User::getToggles();
 		foreach ( $togs as $tname ) {
@@ -517,7 +520,7 @@
 		global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress;
 		global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication;
 		global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth;
-		global $wgEmailConfirmToEdit, $wgAjaxSearch;
+		global $wgEmailConfirmToEdit, $wgAjaxSearch, $wgEnableMWSuggest;
 
 		$wgOut->setPageTitle( wfMsg( 'preferences' ) );
 		$wgOut->setArticleRelated( false );
@@ -980,8 +983,13 @@
 				wfLabel( wfMsg( 'useajaxsearch' ), 'wpUseAjaxSearch' ),
 				wfCheck( 'wpUseAjaxSearch', $this->mUseAjaxSearch, array( 'id' => 'wpUseAjaxSearch' ) )
 			) : '';
+		$mwsuggest = $wgEnableMWSuggest ?
+			$this->addRow(
+				wfLabel( wfMsg( 'mwsuggest-disable' ), 'wpDisableMWSuggest' ),
+				wfCheck( 'wpDisableMWSuggest', $this->mDisableMWSuggest, array( 'id' => 'wpDisableMWSuggest' ) )
+			) : '';
 		$wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'searchresultshead' ) . '</legend><table>' .
-			$ajaxsearch .
+			$ajaxsearch .			
 			$this->addRow(
 				wfLabel( wfMsg( 'resultsperpage' ), 'wpSearch' ),
 				wfInput( 'wpSearch', 4, $this->mSearch, array( 'id' => 'wpSearch' ) )
@@ -994,6 +1002,7 @@
 				wfLabel( wfMsg( 'contextchars' ), 'wpSearchChars' ),
 				wfInput( 'wpSearchChars', 4, $this->mSearchChars, array( 'id' => 'wpSearchChars' ) )
 			) .
+			$mwsuggest.
 		"</table><fieldset><legend>" . wfMsg( 'defaultns' ) . "</legend>$ps</fieldset></fieldset>" );
 
 		# Misc
Index: trunk/phase3/includes/DefaultSettings.php
===================================================================
--- trunk/phase3/includes/DefaultSettings.php	(revision 33399)
+++ trunk/phase3/includes/DefaultSettings.php	(revision 33400)
@@ -1597,7 +1597,33 @@
 
 $wgDisableTextSearch = false;
 $wgDisableSearchContext = false;
+
 /**
+ * Template for OpenSearch suggestions, defaults to API action=opensearch
+ * 
+ * Sites with heavy load would tipically have these point to a custom
+ * PHP wrapper to avoid firing up mediawiki for every keystroke
+ * 
+ * Placeholders: {searchTerms}
+ * 
+ */
+$wgOpenSearchTemplate = false;
+
+/**
+ * Enable suggestions while typing in search boxes 
+ * (results are passed around in OpenSearch format) 
+ */
+$wgEnableMWSuggest = false;
+
+/**
+ *  Template for internal MediaWiki suggestion engine, defaults to API action=opensearch
+ *  
+ *  Placeholders: {searchTerms}, {namespaces}, {dbname}
+ *  
+ */
+$wgMWSuggestTemplate = false;
+
+/**
  * If you've disabled search semi-permanently, this also disables updates to the
  * table. If you ever re-enable, be sure to rebuild the search table.
  */
Index: trunk/phase3/includes/Skin.php
===================================================================
--- trunk/phase3/includes/Skin.php	(revision 33399)
+++ trunk/phase3/includes/Skin.php	(revision 33400)
@@ -300,6 +300,7 @@
 		global $wgUseAjax, $wgAjaxWatch;
 		global $wgVersion, $wgEnableAPI, $wgEnableWriteAPI;
 		global $wgRestrictionTypes, $wgLivePreview;
+		global $wgMWSuggestTemplate, $wgDBname, $wgEnableMWSuggest;
 
 		$ns = $wgTitle->getNamespace();
 		$nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText();
@@ -331,6 +332,13 @@
 			'wgEnableAPI' => $wgEnableAPI,
 			'wgEnableWriteAPI' => $wgEnableWriteAPI,
 		);
+		
+		if( $wgUseAjax && $wgEnableMWSuggest && !$wgUser->getOption( 'disablesuggest', false )){
+			$vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
+			$vars['wgDBname'] = $wgDBname;
+			$vars['wgSearchNamespaces'] = SearchEngine::userNamespaces( $wgUser );
+			$vars['wgMWSuggestMessages'] = array( wfMsg('search-mwsuggest-enabled'), wfMsg('search-mwsuggest-disabled'));
+		}
 
 		foreach( $wgRestrictionTypes as $type )
 			$vars['wgRestriction' . ucfirst( $type )] = $wgTitle->getRestrictions( $type );
Index: trunk/phase3/includes/SpecialSearch.php
===================================================================
--- trunk/phase3/includes/SpecialSearch.php	(revision 33399)
+++ trunk/phase3/includes/SpecialSearch.php	(revision 33400)
@@ -32,10 +32,10 @@
 
 	$search = str_replace( "\n", " ", $wgRequest->getText( 'search', $par ) );
 	$searchPage = new SpecialSearch( $wgRequest, $wgUser );
-	if( $wgRequest->getVal( 'fulltext' ) ||
-		!is_null( $wgRequest->getVal( 'offset' ) ) ||
-		!is_null ($wgRequest->getVal( 'searchx' ) ) ) {
-		$searchPage->showResults( $search );
+	if( $wgRequest->getVal( 'fulltext' ) 
+		|| !is_null( $wgRequest->getVal( 'offset' )) 
+		|| !is_null( $wgRequest->getVal( 'searchx' ))) {
+		$searchPage->showResults( $search, 'search' );
 	} else {
 		$searchPage->goResult( $search );
 	}
@@ -60,7 +60,7 @@
 
 		$this->namespaces = $this->powerSearch( $request );
 		if( empty( $this->namespaces ) ) {
-			$this->namespaces = $this->userNamespaces( $user );
+			$this->namespaces = SearchEngine::userNamespaces( $user );
 		}
 
 		$this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false;
@@ -118,10 +118,11 @@
 	function showResults( $term ) {
 		$fname = 'SpecialSearch::showResults';
 		wfProfileIn( $fname );
+		global $wgOut, $wgUser;
+		$sk = $wgUser->getSkin();
 
 		$this->setupPage( $term );
 
-		global $wgOut;
 		$wgOut->addWikiMsg( 'searchresulttext' );
 
 		if( '' === trim( $term ) ) {
@@ -175,19 +176,24 @@
 			wfProfileOut( $fname );
 			return;
 		}
+		
 		$textMatches = $search->searchText( $rewritten );
 
-		// did you mean...
+		// did you mean... suggestions
 		if($textMatches && $textMatches->hasSuggestion()){
-			global $wgScript;
-			$fulltext = htmlspecialchars(wfMsg('search'));
-			$suggestLink = '<a href="'.$wgScript.'?title=Special:Search&amp;search='.
-				urlencode($textMatches->getSuggestionQuery()).'&amp;fulltext='.$fulltext.'">'
-				.$textMatches->getSuggestionSnippet().'</a>';
+			$st = SpecialPage::getTitleFor( 'Search' );			
+			$stParams = wfArrayToCGI( array( 
+					'search' 	=> $textMatches->getSuggestionQuery(), 
+					'fulltext' 	=> wfMsg('search')),
+					$this->powerSearchOptions());
+					
+			$suggestLink = '<a href="'.$st->escapeLocalURL($stParams).'">'.
+					$textMatches->getSuggestionSnippet().'</a>';
+			 		
 			$wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>');
 		}
 
-
+		// show number of results
 		$num = ( $titleMatches ? $titleMatches->numRows() : 0 )
 			+ ( $textMatches ? $textMatches->numRows() : 0);
 		$totalNum = 0;
@@ -197,7 +203,8 @@
 			$totalNum += $textMatches->getTotalHits();
 		if ( $num > 0 ) {
 			if ( $totalNum > 0 ){
-				$top = wfMsgExt('showingresultstotal',array( 'parseinline' ), $this->offset+1, $this->offset+$num, $totalNum);
+				$top = wfMsgExt('showingresultstotal', array( 'parseinline' ), 
+					$this->offset+1, $this->offset+$num, $totalNum );
 			} elseif ( $num >= $this->limit ) {
 				$top = wfShowingResults( $this->offset, $this->limit );
 			} else {
@@ -206,6 +213,7 @@
 			$wgOut->addHTML( "<p>{$top}</p>\n" );
 		}
 
+		// prev/next links
 		if( $num || $this->offset ) {
 			$prevnext = wfViewPrevNext( $this->offset, $this->limit,
 				SpecialPage::getTitleFor( 'Search' ),
@@ -230,16 +238,23 @@
 		}
 
 		if( $textMatches ) {
+			// output appropriate heading
 			if( $textMatches->numRows() ) {
 				if($titleMatches)
 					$wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
 				else // if no title matches the heading is redundant
-					$wgOut->addHTML("<hr/>");
-				$wgOut->addHTML( $this->showMatches( $textMatches ) );
+					$wgOut->addHTML("<hr/>");								
 			} elseif( $num == 0 ) {
 				# Don't show the 'no text matches' if we received title matches
 				$wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' );
 			}
+			// show interwiki results if any
+			if( $textMatches->hasInterwikiResults() )
+				$wgOut->addHtml( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ));
+			// show results
+			if( $textMatches->numRows() )
+				$wgOut->addHTML( $this->showMatches( $textMatches ) );
+				
 			$textMatches->free();
 		}
 
@@ -255,14 +270,14 @@
 
 	#------------------------------------------------------------------
 	# Private methods below this line
-
+	
 	/**
 	 *
 	 */
 	function setupPage( $term ) {
 		global $wgOut;
 		if( !empty( $term ) )
-			$wgOut->setPageTitle( wfMsg( 'searchresults' ) );
+			$wgOut->setPageTitle( wfMsg( 'searchresults' ) );			
 		$subtitlemsg = ( Title::newFromText( $term ) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
 		$wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ) );
 		$wgOut->setArticleRelated( false );
@@ -270,24 +285,6 @@
 	}
 
 	/**
-	 * Extract default namespaces to search from the given user's
-	 * settings, returning a list of index numbers.
-	 *
-	 * @param User $user
-	 * @return array
-	 * @private
-	 */
-	function userNamespaces( &$user ) {
-		$arr = array();
-		foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
-			if( $user->getOption( 'searchNs' . $ns ) ) {
-				$arr[] = $ns;
-			}
-		}
-		return $arr;
-	}
-
-	/**
 	 * Extract "power search" namespace settings from the request object,
 	 * returning a list of index numbers to search.
 	 *
@@ -319,22 +316,27 @@
 		return $opt;
 	}
 
-
-
 	/**
+	 * Show whole set of results 
+	 * 
 	 * @param SearchResultSet $matches
-	 * @param string $terms partial regexp for highlighting terms
 	 */
 	function showMatches( &$matches ) {
 		$fname = 'SpecialSearch::showMatches';
 		wfProfileIn( $fname );
 
 		global $wgContLang;
-		$tm = $wgContLang->convertForSearchResult( $matches->termMatches() );
-		$terms = implode( '|', $tm );
+		$terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
 
+		$out = "";
+		
+		$infoLine = $matches->getInfo();
+		if( !is_null($infoLine) )
+			$out .= "\n<!-- {$infoLine} -->\n";
+			
+		
 		$off = $this->offset + 1;
-		$out = "<ul start='{$off}' class='mw-search-results'>\n";
+		$out .= "<ul start='{$off}' class='mw-search-results'>\n";
 
 		while( $result = $matches->next() ) {
 			$out .= $this->showHit( $result, $terms );
@@ -351,26 +353,23 @@
 	/**
 	 * Format a single hit result
 	 * @param SearchResult $result
-	 * @param string $terms partial regexp for highlighting terms
+	 * @param array $terms terms to highlight
 	 */
 	function showHit( $result, $terms ) {
 		$fname = 'SpecialSearch::showHit';
 		wfProfileIn( $fname );
 		global $wgUser, $wgContLang, $wgLang;
-
-		$t = $result->getTitle();
-		if( is_null( $t ) ) {
+		
+		if( $result->isBrokenTitle() ) {
 			wfProfileOut( $fname );
 			return "<!-- Broken link in search result -->\n";
 		}
+		
+		$t = $result->getTitle();
 		$sk = $wgUser->getSkin();
 
-		//$contextlines = $wgUser->getOption( 'contextlines',  5 );
-		$contextlines = 2; // Hardcode this. Old defaults sucked. :)
-		$contextchars = $wgUser->getOption( 'contextchars', 50 );
+		$link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
 
-		$link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet());
-
 		//If page content is not readable, just return the title.
 		//This is not quite safe, but better than showing excerpts from non-readable pages
 		//Note that hiding the entry entirely would screw up paging.
@@ -378,15 +377,34 @@
 			return "<li>{$link}</li>\n";
 		}
 
-		$revision = Revision::newFromTitle( $t );
 		// If the page doesn't *exist*... our search index is out of date.
 		// The least confusing at this point is to drop the result.
 		// You may get less results, but... oh well. :P
-		if( !$revision ) {
+		if( $result->isMissingRevision() ) {
 			return "<!-- missing page " .
 				htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
 		}
 
+		// format redirects / relevant sections
+		$redirectTitle = $result->getRedirectTitle();
+		$redirectText = $result->getRedirectSnippet($terms);
+		$sectionTitle = $result->getSectionTitle();
+		$sectionText = $result->getSectionSnippet($terms);
+		$redirect = '';
+		if( !is_null($redirectTitle) )
+			$redirect = "<span class='searchalttitle'>"
+				.wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
+				."</span>";
+		$section = '';
+		if( !is_null($sectionTitle) )
+			$section = "<span class='searchalttitle'>" 
+				.wfMsg('search-section', $sk->makeKnownLinkObj( $sectionTitle, $sectionText))
+				."</span>";
+
+		// format text extract
+		$extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>";
+		
+		// format score
 		if( is_null( $result->getScore() ) ) {
 			// Search engine doesn't report scoring info
 			$score = '';
@@ -396,51 +414,27 @@
 				. ' - ';
 		}
 
-		// try to fetch everything from the search engine backend
-		// then fill-in what couldn't be fetched
-		$extract = $result->getTextSnippet();
+		// format description
 		$byteSize = $result->getByteSize();
 		$wordCount = $result->getWordCount();
 		$timestamp = $result->getTimestamp();
-		$redirectTitle = $result->getRedirectTitle();
-		$redirectText = $result->getRedirectSnippet();
-		$sectionTitle = $result->getSectionTitle();
-		$sectionText = $result->getSectionSnippet();
-
-		// fallback
-		if( is_null($extract) || is_null($wordCount) || is_null($byteSize) ){
-			$text = $revision->getText();
-			if( is_null($extract) )
-				$extract = $this->extractText( $text, $terms, $contextlines, $contextchars );
-			if( is_null($byteSize) )
-				$byteSize = strlen( $text );
-			if( is_null($wordCount) )
-				$wordCount = str_word_count( $text );
-		}
-		if( is_null($timestamp) ){
-			$timestamp = $revision->getTimestamp();
-		}
-
-		// format description
 		$size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ),
 			$sk->formatSize( $byteSize ),
 			$wordCount );
 		$date = $wgLang->timeanddate( $timestamp );
 
-		// format redirects / sections
-		$redirect = '';
-		if( !is_null($redirectTitle) )
-			$redirect = "<span class='searchalttitle'>"
-				.wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
-				."</span>";
-		$section = '';
-		if( !is_null($sectionTitle) )
-			$section = "<span class='searchalttitle'>"
-				.wfMsg('search-section', $sk->makeKnownLinkObj( $sectionTitle, $sectionText))
-				."</span>";
-		// wrap extract
-		$extract = "<div class='searchresult'>".$extract."</div>";
-
+		// link to related articles if supported
+		$related = '';
+		if( $result->hasRelated() ){
+			$st = SpecialPage::getTitleFor( 'Search' );
+			$stParams = wfArrayToCGI( $this->powerSearchOptions(),
+				array('search'    => wfMsg('searchrelated').':'.$t->getPrefixedText(),
+				      'fulltext'  => wfMsg('search') ));
+			
+			$related = ' -- <a href="'.$st->escapeLocalURL($stParams).'">'. 
+				wfMsg('search-relatedarticle').'</a>';
+		}
+				
 		// Include a thumbnail for media files...
 		if( $t->getNamespace() == NS_IMAGE ) {
 			$img = wfFindFile( $t );
@@ -462,7 +456,7 @@
 						'<td valign="top">' .
 						$link .
 						$extract .
-						"<div class='mw-search-result-data'>{$score}{$desc} - {$date}</div>" .
+						"<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
 						'</td>' .
 						'</tr>' .
 						'</table>' .
@@ -473,55 +467,109 @@
 
 		wfProfileOut( $fname );
 		return "<li>{$link} {$redirect} {$section} {$extract}\n" .
-			"<div class='mw-search-result-data'>{$score}{$size} - {$date}</div>" .
+			"<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
 			"</li>\n";
 
 	}
 
-	private function extractText( $text, $terms, $contextlines, $contextchars ) {
-		global $wgLang, $wgContLang;
-		$fname = __METHOD__;
+	/**
+	 * Show results from other wikis
+	 * 
+	 * @param SearchResultSet $matches
+	 */
+	function showInterwiki( &$matches, $query ) {
+		$fname = 'SpecialSearch::showInterwiki';
+		wfProfileIn( $fname );
 
-		$lines = explode( "\n", $text );
+		global $wgContLang;
+		$terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
 
-		$max = intval( $contextchars ) + 1;
-		$pat1 = "/(.*)($terms)(.{0,$max})/i";
+		$out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".wfMsg('search-interwiki-caption')."</div>\n";		
+		$off = $this->offset + 1;
+		$out .= "<ul start='{$off}' class='mw-search-iwresults'>\n";
 
-		$lineno = 0;
+		// work out custom project captions
+		$customCaptions = array();
+		$customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption>
+		foreach($customLines as $line){
+			$parts = explode(":",$line,2);
+			if(count($parts) == 2) // validate line
+				$customCaptions[$parts[0]] = $parts[1]; 
+		}
+		
+		
+		$prev = null;
+		while( $result = $matches->next() ) {
+			$out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
+			$prev = $result->getInterwikiPrefix();
+		}
+		// FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
+		$out .= "</ul></div>\n";
 
-		$extract = "";
-		wfProfileIn( "$fname-extract" );
-		foreach ( $lines as $line ) {
-			if ( 0 == $contextlines ) {
-				break;
-			}
-			++$lineno;
-			$m = array();
-			if ( ! preg_match( $pat1, $line, $m ) ) {
-				continue;
-			}
-			--$contextlines;
-			$pre = $wgContLang->truncate( $m[1], -$contextchars, ' ... ' );
+		// convert the whole thing to desired language variant
+		global $wgContLang;
+		$out = $wgContLang->convert( $out );
+		wfProfileOut( $fname );
+		return $out;
+	}
+	
+	/**
+	 * Show single interwiki link
+	 *
+	 * @param SearchResult $result
+	 * @param string $lastInterwiki
+	 * @param array $terms
+	 * @param string $query 
+	 * @param array $customCaptions iw prefix -> caption
+	 */
+	function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions){
+		$fname = 'SpecialSearch::showInterwikiHit';
+		wfProfileIn( $fname );
+		global $wgUser, $wgContLang, $wgLang;
+		
+		if( $result->isBrokenTitle() ) {
+			wfProfileOut( $fname );
+			return "<!-- Broken link in search result -->\n";
+		}
+		
+		$t = $result->getTitle();
+		$sk = $wgUser->getSkin();
+		
+		$link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
+				
+		// format redirect if any
+		$redirectTitle = $result->getRedirectTitle();
+		$redirectText = $result->getRedirectSnippet($terms);
+		$redirect = '';
+		if( !is_null($redirectTitle) )
+			$redirect = "<span class='searchalttitle'>"
+				.wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
+				."</span>";
 
-			if ( count( $m ) < 3 ) {
-				$post = '';
-			} else {
-				$post = $wgContLang->truncate( $m[3], $contextchars, ' ... ' );
-			}
-
-			$found = $m[2];
-
-			$line = htmlspecialchars( $pre . $found . $post );
-			$pat2 = '/(' . $terms . ")/i";
-			$line = preg_replace( $pat2,
-			  "<span class='searchmatch'>\\1</span>", $line );
-
-			$extract .= "${line}\n";
+		$out = "";
+		// display project name 
+		if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()){
+			if( key_exists($t->getInterwiki(),$customCaptions) )
+				// captions from 'search-interwiki-custom'
+				$caption = $customCaptions[$t->getInterwiki()];
+			else{
+				// default is to show the hostname of the other wiki which might suck 
+				// if there are many wikis on one hostname
+				$parsed = parse_url($t->getFullURL());
+				$caption = wfMsg('search-interwiki-default', $parsed['host']); 
+			}		
+			// "more results" link (special page stuff could be localized, but we might not know target lang)
+			$searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");   			
+			$searchLink = $sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'),
+				wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search'))); 
+			$out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>{$searchLink}</span>{$caption}</div>\n<ul>";
 		}
-		wfProfileOut( "$fname-extract" );
 
-		return $extract;
+		$out .= "<li>{$link} {$redirect}</li>\n"; 
+		wfProfileOut( $fname );
+		return $out;
 	}
+	
 
 	/**
 	 * Generates the power search box at bottom of [[Special:Search]]
@@ -575,7 +623,7 @@
 			'action' => $wgScript
 		));
 		$out .= Xml::hidden( 'title', 'Special:Search' );
-		$out .= Xml::input( 'search', 50, $term ) . ' ';
+		$out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . ' ';
 		foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
 			if( in_array( $ns, $this->namespaces ) ) {
 				$out .= Xml::hidden( "ns{$ns}", '1' );
Index: trunk/phase3/opensearch_desc.php
===================================================================
--- trunk/phase3/opensearch_desc.php	(revision 33399)
+++ trunk/phase3/opensearch_desc.php	(revision 33400)
@@ -16,7 +16,7 @@
 $title = SpecialPage::getTitleFor( 'Search' );
 $template = $title->escapeFullURL( 'search={searchTerms}' );
 
-$suggest = htmlspecialchars($wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}');
+$suggest = htmlspecialchars(SearchEngine::getOpenSearchTemplate() );
 
 
 $response = $wgRequest->response();
Index: trunk/phase3/languages/messages/MessagesEn.php
===================================================================
--- trunk/phase3/languages/messages/MessagesEn.php	(revision 33399)
+++ trunk/phase3/languages/messages/MessagesEn.php	(revision 33400)
@@ -1297,7 +1297,16 @@
 'search-redirect'       => '(redirect $1)',
 'search-section'        => '(section $1)',
 'search-suggest'        => 'Did you mean: $1',
+'search-interwiki-caption' => 'Sister projects',
+'search-interwiki-default' => "$1 results:",
+'search-interwiki-custom'  => '', # do not translate or duplicate this message to other languages
+'search-interwiki-more'    => '(more)',
+'search-mwsuggest-enabled' => 'with suggestions',
+'search-mwsuggest-disabled'=> 'no suggestions',
+'search-relatedarticle'    => 'Related',
+'mwsuggest-disable'     => 'Disable AJAX suggestions',  
 'searchall'             => 'all',
+'searchrelated'         => 'related',
 'showingresults'        => "Showing below up to {{PLURAL:$1|'''1''' result|'''$1''' results}} starting with #'''$2'''.",
 'showingresultsnum'     => "Showing below {{PLURAL:$3|'''1''' result|'''$3''' results}} starting with #'''$2'''.",
 'showingresultstotal'   => "Showing below results '''$1 - $2''' of '''$3'''",

Follow-up revisions

Rev.Commit summaryAuthorDate
r33414* Update messageTypes.inc per r33400...raymond05:05, 16 April 2008
r34210Re-commit r34072 with some modifications:...rainman15:31, 4 May 2008

Comments

#Comment by Simetrical (Talk | contribs)   03:00, 16 July 2009

I adjusted some capability-testing in r53347, since it was using the old broken is_khtml variable that I wanted to remove (and did remove in r53348). Could you please check that the change I made actually works? The theory seems sound, but I didn't test its applicability in this specific case, since I wasn't totally sure what the code was actually supposed to do.

Status & tagging log

  • 15:26, 12 September 2011 Meno25 (Talk | contribs) changed the status of r33400 [removed: ok added: old]
Personal tools
Namespaces
Variants
Views
Actions
Site
Support
Download
Development
Communication
Toolbox