grep その6

options.add の 使い方が正しく無いようです。

windowが 複数ある状態でset xxxxx を 使用すると、
setter が 無限にcallされます。(メモリを食い潰して、落ちます)


とりあず、強引に修正しました。

主な修正点は、
OPTION_SCOPE_BOTH を OPTION_SCOPE_GLOBAL に変更です。
(3回くらいsetterが 呼ばれてしまうので、無限回と比較して
まともという程度です)


その5を利用しているかたがいらっしゃいましたら、
下記コードか以前のものを利用して下さい。


時間がとれたら、set を正しく動くようになおすか、
grep_option のような独自コマンドで設定するようにします。

// 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_clear_editor_range = true;
	const option_scroll = "grep_scroll_position";
	const option_grep = "grep_option";
	const option_highlight = "hlgrep";

	//{{{ options

	(function(){
    const scope = options.OPTION_SCOPE_GLOBAL;
		const position_list = {
			"top": [0, -1],
			"center": [50, -1],
			"bottom": [100, -1],
			"auto"  : [-1, -1],
		};

		const grep_groups={
			"engine":{
				n: "normal search",
				r: "regex search",
				x: "xul/migemo search",
			},
			"flags":{
				i: "ignore case",
				m: "multiple lines",
			},
		};
		options.remove(option_scroll);
		options.add([option_scroll],"grep scroll postion",
		"string","center",
		{
			scope: scope,
			completer:function(context){
				context.completions = ([[x,""] for(x in position_list)]);
			},
			validator:Option.validateCompleter,
		});
		options.get(option_scroll).__defineGetter__("vhPercent",function() position_list[this.value]||[-1,-1]);

		options.remove(option_grep);
		options.add([option_grep], "grep option",
		"charlist", "imx",
		{
			scope: scope,
			setter:function(value){ 
				let list = [];
				for(let [,c] in Iterator(value||[])){
					if(list.indexOf(c)===-1) list.push(c);
				}
				return list.join(""); 
			},
			completer:function(context){
				for(let [grp_nm, grp_opt] in Iterator(grep_groups)){
					context.fork(grp_nm,0,this,function(context){
						context.title[0] = grp_nm;
						context.completions = [val for(val in Iterator(grp_opt))];
					});
				}
			},
			validator:function(val) Option.validateCompleter.call(this,val)
				&& [x for([x,] in grep_groups.engine)].some(function(n) val.indexOf(n) >= 0)
      ,
		});

		options.remove(option_highlight);
		options.add([option_highlight],"grep highlight",
		"boolean", false,
		{
			scope: scope,
			setter:function(value){
				if(value){
					let info = get_cache();
					if(info) show_highlight(info.list);
				}
				else
					clear_highlight();
				return value;
			},
			completer:function()[
				[true , "show highlight"],
				[false, "hide highlight"],
			],
		});
	})();
	//}}}

	//{{{core
	const vimp = "liberator-completions";
	let finder = Cc["@mozilla.org/embedcomp/rangefind;1"]
		.getService(Ci.nsIFind);
	const option_list = "imxnr";
  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;
		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 = options[option_grep];
			}else if(m[6]){
				s = m[6];
			}else if(m[3]){
				s = m[3];
			}
			flags = "";
			if(/i/.test(s)) flags += "i";
			if(/m/.test(s)) flags += "m";
			if(m=/.+([xnr])/.exec(options[option_grep]+s)){
				mode = m[1];
			}
		}
    let option = <>{flags}{mode}</>.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;

		let(op=options.get(option_scroll).vhPercent)
		selection
			.QueryInterface(Ci.nsISelection2)
			.scrollIntoView(Ci.nsISelectionController.SELECTION_ANCHOR_REGION,true,op[0],op[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(options[option_highlight]){
          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;
				}
			}
		});
	} //}}}
})();