grep

ページをgrep するコマンド です。


competerで ヒット 一覧を 見ることができます。

オプション

ringo → 標準指定(vimp_grep_default_option)で検索(標準指定無しならimx)
ringo/ix → migemo で 大文字小文字無視検索
ringo/n → 標準検索

or 検索

りんご\ みかん/n の形で りんご または みかん を 検索します
migemomigemo.queryFunctional で 同様のことができるはずなんですが、
私の環境だとエラーがでて使えなかったので、未実装です。
セパレータのスペースは\無しできるとよかったのですけど、
やりかたがさっぱりでした。

use_hook_search_map

true で grep で ジャンプした後、/を押されるまで、
nN で grep の 結果を 前後移動できます。

use_show_echo

true で ジャンプ後、liberator.echoで ハイライト表示されます。
ただ、一行しか生成していないのに、いちいちなぜか2行になるので微妙です。

補完機能

を 押すと 先頭に数字 そして オプションが検索ワードの前に移るようにしてます。
数字は、何番目のヒットにジャンプするかの指定になります。
再度末尾にオプションを指定するとそちらが優先されます。

正規表現での検索

試行錯誤しましたが

  1. ページ全体 の文字列を取得
  2. 正規表現で文字を抽出
  3. 各文字で検索し 範囲を取得
  4. 取得範囲をソートする

と かなり力技となっておりますorz


また、検索結果を保持する適所がわからなかったので、
content.document 直下を間借りしております。
どこが良いんでしょうかね…

// vim: set fdm=marker:
(function(){
	//{{{ config
	const cmd_name = ["grep"];
	const match_style = "color:red;font-weight:bold;";
	const abbr_style = "color:blue;font-weight:bold;";
	const grep_info_attr = "__vmp_grep_cmd";
	const use_hook_search_map = true;
	const use_show_echo = false;
	const use_highlight_option = true;
	const use_clear_editor_range = true;
	let screen_position = [-1, -1];//[垂直,水平]
	let default_option = liberator.globalVariables.vimp_grep_default_option|| "imx";

	//{{{core
	const vimp = "liberator-completions";
	let finder = Cc["@mozilla.org/embedcomp/rangefind;1"]
		.getService(Ci.nsIFind);
	const option_list = "imxnr" + (use_highlight_option ? "h" : "");
  const gFm = Cc["@mozilla.org/focus-manager;1"]
              .getService(Ci.nsIFocusManager);
	//}}}
	//}}}

	function range2string(r, max){
		let txt = r.toString();
		let so = r.startOffset;
		let eo = r.endOffset;
		let et = r.endContainer.textContent;
		let st = r.startContainer.textContent;
		let max = (arguments.callee.max/2-2) || 0;

		return { 
			abbr : so > max,
			prev : (so > max ? st.substr(so - max, max) : st.substr(0, so)).replace(/\s+$/,"\xa0"),
			text : r.toString(),
			next : et.substr(eo, max*2).replace(/^\s+/,"\xa0"),
		};
	}

	function get_word_max(){
		let win = document.getElementById(vimp).contentWindow;
		let fontSize = win.getComputedStyle(win.document.body,'').fontSize.replace("px","");
		return Math.floor(document.width / fontSize);
	}

	let process = [function(i,text){
		let r = range2string(i.item.range);
	
		return <><div style="position:absolute;">
			{r.abbr ? <span style={abbr_style}>~~</span> : ""}
			{r.prev}
			<span style={match_style}>{r.text}</span>
			{r.next}
			</div>
		</>;
	}, 
		function() <span></span>];

	function iteratorFrame(win){
		yield win;
		for(let f1 in util.Array.itervalues(win.frames)){
			for(let f2 in arguments.callee(f1)) yield f2;
		}
	}

	function create_ranges(win){
		let doc = win.document;
		let body = doc.body;
		let count = body.childNodes.length;

		let searchRange = doc.createRange();
		let startPt = doc.createRange();
		let endPt = doc.createRange();
		
		searchRange.setStart(body, 0);
		searchRange.setEnd(body, count);
		
		startPt.setStart(body, 0);
		startPt.collapse(true);
		endPt.setStart(body, count);
		endPt.collapse(true);
		return [searchRange, startPt, endPt];
	}

	function normal_find(win, word, option){
		let doc = win.document;
		let list = [];
		let [whole,start,end] = create_ranges(win);

		while(result = finder.Find(word, whole, start, end)){
			list.push(result);
			start = doc.createRange();
			start.setStart(result.endContainer, result.endOffset);
			start.collapse(true);
		}
		return list;
	}

	function normal_grep(word, option){
		let result,ret=[];
		finder.caseSensitive = !/i/.test(option);
		for(let win in iteratorFrame(content.window)){
			let list = [];
			let words = word.split(" ");

			//unique
			words = words.sort();
			let(prev) words = words.filter(function(n) prev !== (prev=n));

			for([,w] in Iterator(words)){
				list = list.concat(normal_find(win, w));
			}
			list.sort(function(a,b) a.compareBoundaryPoints(Range.START_TOSTART, b));
			ret = ret.concat(list);
		}
		return ret;
	}

//{{{
	function regexp_grep_core(re){
		let result,ret=[];
		finder.caseSensitive = true;
		for(let win in iteratorFrame(content.window)){
			let list = [];
			let [whole,start,end] = create_ranges(win);
			let words = whole.toString().match(re)||[];

			//unique
			words = words.sort();
			let(prev) words = words.filter(function(n) prev !== (prev=n));

			for([,w] in Iterator(words)){
				list = list.concat(normal_find(win, w));
			}
			list.sort(range_compare);
			ret = ret.concat(list);
		}
		return ret;
	}

	function regexp_grep(word, option){
		return regexp_grep_core(RegExp(word, option));
	}

	function migemo_grep(word, option){
		let re = new RegExp(window.migemo.query(word), option);
		return regexp_grep_core(re);
	}
	//}}}

	function range_compare(a,b) 
		a.compareBoundaryPoints(Range.START_TO_START, b) 
      || a.compareBoundaryPoints(Range.END_TO_END, b) 

	function range_compare_d(win){ 
    win = win || content.window;
    let list = [w.document for(w in iteratorFrame(win))];
    return function(a,b){
      let n1 = a.startContainer.ownerDocument;
      let n2 = b.startContainer.ownerDocument;
      let ret=0;

      if(n1 === n2) return a.compareBoundaryPoints(Range.START_TO_START, b) 
        || a.compareBoundaryPoints(Range.END_TO_END, b);
      for(let [,n] in Iterator(list)){
        if(n===n1) return -1;
        else if(n===n2) return 1;
      }
    };
  }

	let mode_func = { };
	mode_func.n = normal_grep;
	mode_func.r = regexp_grep;
	mode_func.x = migemo_grep;

	//{{{ range cache
	function get_cache(){
		return content.document[grep_info_attr];
	}

	function get_grep_info(arg){
		let m,word,flags,mode,num,opt;
		let re = new RegExp(
		<>^((\d*)([{option_list}]*)/)?([^/]+)(/([{option_list}]*))?$</>.toString()
		,"i");
		if(m=re.exec(arg)){
			let s = "";
			word = m[4];
			num = m[2]||0;
			if(!m[1] && !m[5]){
				s = default_option;
			}else if(m[6]){
				s = m[6];
			}else if(m[3]){
				s = m[3];
			}
			flags = "";
      opt = "";
			if(/i/.test(s)) flags += "i";
			if(/m/.test(s)) flags += "m";
			if(/h/.test(s)) opt   += "h";
			if(m=/.+([xnr])/.exec("n"+default_option+s)){
				mode = m[1];
			}
		}
    let option = <>{flags}{mode}{opt}</>.toString();
		let info = get_cache();
		if(info){
			if(info.word === word && info.option === option){
				info.index = num;
				info.enabled = false;
				return info;
			}
		}

		let func = mode_func[mode];
		if(!func){
			liberator.echoerr(<>{mode} is not support</>.toString());
			return;
		}

		let list = func(word, flags + "g");
		info = {list:list, index:num, word: word, option: option};
		content.document[grep_info_attr] = info;
		return info;
	}

	function getSelectionControllerFromWindow(view){
		let selectionController = null;
		try{
		selectionController = view
			.QueryInterface(Ci.nsIInterfaceRequestor)
			.getInterface(Ci.nsIWebNavigation)
			.QueryInterface(Ci.nsIDocShell)
			.QueryInterface(Ci.nsIInterfaceRequestor)
			.getInterface(Ci.nsISelectionDisplay)
			.QueryInterface(Ci.nsISelectionController);
		}catch(ex){}

		return selectionController;
	}

	function checkEditableElement(node){
		let ret = false;
		try{
			node.QueryInterface(Ci.nsIDOMNSEditableElement)
			ret = true;
		}catch(ex){}
		return ret;
	}

	function getEditor(node){
		while(node){
			if(checkEditableElement(node)){
				return node.editor;
			}
			node = node.parentNode;
		}
		return null;
	}

	function getSelectionControllerFromRange(aRange){
		let node = aRange.startContainer;
		let editor = getEditor(node);
		return editor ? editor.selectionController 
			: getSelectionControllerFromWindow(node.ownerDocument.defaultView);
	}

	function clearEditorAllSelections(win, aType){
		win = win || content.window;
		aType = aType || Ci.nsISelectionController.SELECTION_NORMAL;
		for(let w in iteratorFrame(win)){
			let list = w.document.getElementsByTagName("*");
			for(let [,n] in Iterator(list)){
				if(checkEditableElement(n)){
					let editor = n.editor;
					if(editor){
						let selection = editor.selectionController.getSelection(aType);
						if(selection.rangeCount>0){
							selection.removeAllRanges();
						}
					}
				}
			}
		}
	}

	function grep_jump(info){
		if(info.list.length == 0){
			liberator.echoerr(<>no match "{info.option}/{info.word}"</>);
			return;
		}
		let n = info.list[info.index];
		if(!n){
			liberator.echoerr(<>index error "{info.index}"</>);
			return;
		}
		let win = n.startContainer.ownerDocument.defaultView;

    if(gFm.focusedWindow){
			if(use_clear_editor_range) clearEditorAllSelections();
      gFm.focusedWindow.getSelection().removeAllRanges();
    }
		let selectionController = getSelectionControllerFromWindow(win);
		if(!selectionController) return;

		//{{{ editable element
		let editor = getEditor(n.startContainer);
		if(editor){
			let selectionController = editor.selectionController;
			let selection = selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
			selection.removeAllRanges();
			selection.addRange(n);
			selectionController.setDisplaySelection(Ci.nsISelectionController.SELECTION_ATTENTION);
		}
		//}}} 
		
		let selection = selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
		selection.removeAllRanges();
		
		selection.addRange(n);
		gFm.moveFocus(win, null, Ci.nsIFocusManager.MOVEFOCUS_CARET,
			Ci.nsIFocusManager.FLAG_NOSCROLL | Ci.nsIFocusManager.FLAG_NOSWITCHFRAME);

		selectionController.setDisplaySelection(Ci.nsISelectionController.SELECTION_ATTENTION);
		selectionController.repaintSelection(Ci.nsISelectionController.SELECTION_NORMAL);

		info.enabled = true;

		selection
			.QueryInterface(Ci.nsISelection2)
			.scrollIntoView(Ci.nsISelectionController.SELECTION_ANCHOR_REGION,true,screen_position[0],screen_position[1]);
    win.focus();// nsIFocusManager の focusedWindow を 更新
				
		if(use_show_echo){
			range2string.max = get_word_max();
			liberator.echo(process[0]({item:{range:n}},"").*,4);
		}
	}

	function bi_search(list, n, backword){
		let lhs = 0,rhs = list.length,mid=0,ret;
    let comp = range_compare_d(content.window);
		while(1){
			if(mid===(mid=Math.floor((lhs+rhs)/2))){
        if(mid === 0 && ret < 0) return -1;
        else if(mid === list.length-1 && ret > 0 && !backword) return mid+1;
        else return mid;
			}
			ret = comp(n, list[mid]);
			if(ret < 0) rhs = mid;
			else if(ret > 0) lhs = mid;
			else{ 
        return backword ? mid - 1 : mid;
      }
		}
	}

  function get_grep_list_index(info, isReverse){
    info = info || get_cache();

    let win = gFm.focusedWindow;
    let selection = win.getSelection();
    let r;
    if(selection.rangeCount === 0){
      r = win.document.createRange();
      r.setStart(win.document.body, 0);
    }else{
      r = selection.getRangeAt(0);
    }
    let index = bi_search(info.list, r, isReverse);

    return index;
  }

	function grep_next(){
		let info = get_cache();
		if(!info) return;
    info.index = get_grep_list_index(info);

		if(++info.index >= info.list.length){
			info.index = 0;
			liberator.echoerr("bottom!");
		}
		grep_jump(info);

		return true;
	}

	function grep_prev(){
		let info = get_cache();
		if(!info) return;
    info.index = get_grep_list_index(info, true);
		if(info.index < 0){
			info.index = info.list.length-1;
			liberator.echoerr("top!");
		}
    grep_jump(info);

		return true;
	}
	//}}}
		
  //{{{ highlight
  function show_highlight(list){
    for(let [,r] in Iterator(list)){
      let selectionController,n;
			n = r.startContainer;
			let editor = getEditor(n);
			selectionController = editor
				? editor.selectionController
				: getSelectionControllerFromWindow(n.ownerDocument.defaultView);

			if(selectionController){
        let selection = selectionController.getSelection(Ci.nsISelectionController.SELECTION_FIND);
				selection.addRange(r);
			}
    }
  }

  function clear_selection_find(win){
    win = win||content.window;
		let selectionController = getSelectionControllerFromWindow(win);
		if(selectionController){
			let selection = selectionController.getSelection(Ci.nsISelectionController.SELECTION_FIND);
			selection.removeAllRanges();
		}
  }

  function clear_highlight(){
    for(let w in iteratorFrame(content.window)){
      clear_selection_find(w);
    }
		clearEditorAllSelections(null, Ci.nsISelectionController.SELECTION_FIND);
  }
  //}}}

	let T={
		name : cmd_name ,
		desc :"grep page",
		action:function(args){
			let info = get_grep_info(args[0]);
			if(info){
        if(/h/.test(info.option)){
          clear_highlight();
          show_highlight(info.list);
        }
        grep_jump(info);
      }
		},
		option:{ }
	};

	T.option.args = 1;
	T.option.completer = function(context, args){
		try{
			let info = get_grep_info(args[0]);
			if(!info || info.list.length == 0) return;

			context.process = process;
			context.keys={text:"text",description:"text"};
			context.match = function(str){ return true; };

			range2string.max = get_word_max();

			let query = info.query;
			context.completions = info.list.map(function(n,i){
				return {
					text: <>{i}{info.option}/{info.word}</>.toString(), 
					range: n 
				}
			});
		}catch(ex){liberator.echoerr(ex);}
	};
	commands.addUserCommand(T.name, T.desc, T.action, T.option, true);
	
	if(use_hook_search_map)//{{{
	{
		function hook_function(obj, attr, func){
			const org = "__original";
			let original = obj[attr];
			if(org in original) original = original[org];
			let ret=function(){
				if(!(func.apply(this,arguments)===true)){
					original.apply(this,arguments);
				}
			};
			ret[org] = original;
			obj[attr] = ret;
		}

		map = mappings.get(modes.NORMAL,"/");
		hook_function(map, "action", function(){
			let info = get_cache();
			if(info) info.enabled = false;
		});
		map = mappings.get(modes.NORMAL,"n");
		hook_function(map, "action", function(){
			if(grep_info_attr in content.document){
				let info = get_cache();
				if(info.enabled){
					grep_next();
					return true;
				}
			}
		});
		map = mappings.get(modes.NORMAL,"N");
		hook_function(map, "action", function(){
			if(grep_info_attr in content.document){
				let info = get_cache();
				if(info.enabled){
					grep_prev();
					return true;
				}
			}
		});
	} //}}}
	if(use_highlight_option)//{{{
	{
    function clear_action(){
      clear_highlight();
    }

		commands.addUserCommand(["cleargrep","cgrep"],"clear grep highlight", function() clear_action(), null, true);
	}//}}}
})();