|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
define([ |
|
'require', |
|
'jquery', |
|
'base/js/namespace', |
|
'notebook/js/cell', |
|
'notebook/js/codecell', |
|
'codemirror/lib/codemirror', |
|
|
|
|
|
|
|
'codemirror/addon/selection/mark-selection' |
|
], function ( |
|
requirejs, |
|
$, |
|
Jupyter, |
|
cell, |
|
codecell, |
|
CodeMirror |
|
) { |
|
'use strict'; |
|
|
|
var Cell = cell.Cell; |
|
var CodeCell = codecell.CodeCell; |
|
|
|
var mod_name = 'highlight_selected_word'; |
|
var log_prefix = '[' + mod_name + ']'; |
|
var menu_toggle_class = 'highlight_selected_word_toggle'; |
|
|
|
|
|
|
|
var params = { |
|
highlight_across_all_cells: true, |
|
enable_on_load : true, |
|
code_cells_only: false, |
|
delay: 100, |
|
words_only: false, |
|
highlight_only_whole_words: true, |
|
min_chars: 2, |
|
show_token: '[\\w$]', |
|
highlight_color: '#90EE90', |
|
highlight_color_blurred: '#BBFFBB', |
|
highlight_style: 'matchhighlight', |
|
trim: true, |
|
use_toggle_hotkey: false, |
|
toggle_hotkey: 'alt-h', |
|
outlines_only: false, |
|
outline_width: 2, |
|
only_cells_in_scroll: true, |
|
scroll_min_delay: 100, |
|
hide_selections_in_unfocussed: false, |
|
}; |
|
|
|
|
|
var action_names = { |
|
toggle: '', |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var globalState = { |
|
active: false, |
|
timeout: null, |
|
scrollTimeout: null, |
|
overlay: null, |
|
}; |
|
|
|
|
|
CodeMirror.defineOption("highlightSelectionMatchesInJupyterCells", false, function (cm, val, old) { |
|
if (old && old != CodeMirror.Init) { |
|
globalState.active = false; |
|
|
|
get_relevant_cells().forEach(function (cell, idx, array) { |
|
cell.code_mirror.removeOverlay(mod_name); |
|
}); |
|
globalState.overlay = null; |
|
clearTimeout(globalState.timeout); |
|
globalState.timeout = null; |
|
cm.off("cursorActivity", callbackCursorActivity); |
|
cm.off("focus", callbackOnFocus); |
|
} |
|
if (val) { |
|
if (cm.hasFocus()) { |
|
globalState.active = true; |
|
highlightMatchesInAllRelevantCells(cm); |
|
} |
|
else { |
|
cm.on("focus", callbackOnFocus); |
|
} |
|
cm.on("cursorActivity", callbackCursorActivity); |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function callbackCursorActivity (cm) { |
|
if (globalState.active || cm.hasFocus()) { |
|
scheduleHighlight(cm); |
|
} |
|
} |
|
|
|
function callbackOnFocus (cm) { |
|
|
|
|
|
globalState.active = true; |
|
scheduleHighlight(cm); |
|
} |
|
|
|
function scheduleHighlight (cm) { |
|
clearTimeout(globalState.timeout); |
|
globalState.timeout = setTimeout(function () { highlightMatchesInAllRelevantCells(cm); }, params.delay); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function highlightMatchesInAllRelevantCells (cm) { |
|
var newOverlay = null; |
|
|
|
var re = params.show_token === true ? /[\w$]/ : params.show_token; |
|
var from = cm.getCursor('from'); |
|
if (!cm.somethingSelected() && params.show_token) { |
|
var line = cm.getLine(from.line), start = from.ch, end = start; |
|
while (start && re.test(line.charAt(start - 1))) { |
|
--start; |
|
} |
|
while (end < line.length && re.test(line.charAt(end))) { |
|
++end; |
|
} |
|
if (start < end) { |
|
newOverlay = makeOverlay(line.slice(start, end), re, params.highlight_style); |
|
} |
|
} |
|
else { |
|
var to = cm.getCursor("to"); |
|
if (from.line == to.line) { |
|
if (!params.words_only || isWord(cm, from, to)) { |
|
var selection = cm.getRange(from, to); |
|
if (params.trim) { |
|
selection = selection.replace(/^\s+|\s+$/g, ""); |
|
} |
|
if (selection.length >= params.min_chars) { |
|
var hasBoundary = params.highlight_only_whole_words ? (re instanceof RegExp ? re : /[\w$]/) : false; |
|
newOverlay = makeOverlay(selection, hasBoundary, params.highlight_style); |
|
} |
|
} |
|
} |
|
} |
|
|
|
var siterect = document.getElementById('site').getBoundingClientRect(); |
|
var viewtop = siterect.top, viewbot = siterect.bottom; |
|
var cells = params.highlight_across_all_cells ? get_relevant_cells() : [ |
|
$(cm.getWrapperElement()).closest('.cell').data('cell') |
|
]; |
|
cells.forEach(function (cell, idx, array) { |
|
|
|
cell.code_mirror.operation(function () { |
|
cell.code_mirror.removeOverlay(mod_name); |
|
if (newOverlay && is_in_view(cell.element[0], viewtop, viewbot)) { |
|
cell.code_mirror.addOverlay(newOverlay); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function isWord (cm, from, to) { |
|
var str = cm.getRange(from, to); |
|
if (str.match(/^\w+$/) !== null) { |
|
var pos, chr; |
|
if (from.ch > 0) { |
|
pos = {line: from.line, ch: from.ch - 1}; |
|
chr = cm.getRange(pos, from); |
|
if (chr.match(/\W/) === null) { |
|
return false; |
|
} |
|
} |
|
if (to.ch < cm.getLine(from.line).length) { |
|
pos = {line: to.line, ch: to.ch + 1}; |
|
chr = cm.getRange(to, pos); |
|
if (chr.match(/\W/) === null) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
return false; |
|
} |
|
function boundariesAround (stream, re) { |
|
return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) && |
|
(stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos))); |
|
} |
|
function makeOverlay (query, hasBoundary, style) { |
|
return { |
|
name: mod_name, |
|
token: function (stream) { |
|
if (stream.match(query) && |
|
(!hasBoundary || boundariesAround(stream, hasBoundary))) { |
|
return style; |
|
} |
|
stream.next(); |
|
if (!stream.skipTo(query.charAt(0))) { |
|
stream.skipToEnd(); |
|
} |
|
} |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
var is_in_view = function (elem, viewtop, viewbot) { |
|
var rect = elem.getBoundingClientRect(); |
|
|
|
return (rect.top < viewbot) && (rect.bottom > viewtop) && rect.height; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function get_relevant_cells () { |
|
var cells = Jupyter.notebook.get_cells(); |
|
return params.code_cells_only ? cells.filter(function (c) { return (c instanceof CodeCell); }) : cells; |
|
} |
|
|
|
function add_menu_item () { |
|
if ($('#view_menu').find('.' + menu_toggle_class).length < 1) { |
|
var menu_item = $('<li/>') |
|
.appendTo('#view_menu'); |
|
var menu_link = $('<a/>') |
|
.text('Highlight selected word') |
|
.addClass(menu_toggle_class) |
|
.attr({ |
|
title: 'Highlight all instances of the selected word in the current editor', |
|
href: '#', |
|
}) |
|
.on('click', function () { toggle_highlight_selected(); }) |
|
.appendTo(menu_item); |
|
$('<i/>') |
|
.addClass('fa menu-icon pull-right') |
|
.css({'margin-top': '-2px', 'margin-right': '-16px'}) |
|
.prependTo(menu_link); |
|
} |
|
} |
|
|
|
var throttled_highlight = (function () { |
|
var last, throttle_timeout; |
|
return function throttled_highlight (cm) { |
|
var now = Number(new Date()); |
|
var do_it = function () { |
|
last = Number(new Date()); |
|
highlightMatchesInAllRelevantCells(cm); |
|
}; |
|
var remaining = last + params.scroll_min_delay - now; |
|
if (last && remaining > 0) { |
|
clearTimeout(throttle_timeout); |
|
throttle_timeout = setTimeout(do_it, remaining); |
|
} |
|
else { |
|
last = undefined; |
|
do_it(); |
|
} |
|
} |
|
})(); |
|
|
|
function scroll_handler (evt) { |
|
if (globalState.active && Jupyter.notebook.mode === 'edit' && globalState.overlay) { |
|
|
|
|
|
|
|
var siterect = document.getElementById('site').getBoundingClientRect(); |
|
get_relevant_cells().forEach(function (cell) { |
|
var cm = cell.code_mirror; |
|
if (is_in_view(cell.element, siterect.top, siterect.bot)) { |
|
var need_it = !cm.state.overlays.some(function(ovr) { |
|
return ovr.modeSpec.name === mod_name; }); |
|
if (need_it) cm.addOverlay(globalState.overlay); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
function toggle_highlight_selected (set_on) { |
|
set_on = (set_on !== undefined) ? set_on : !params.enable_on_load; |
|
|
|
if (set_on !== params.enable_on_load) { |
|
params.enable_on_load = set_on; |
|
Jupyter.notebook.config.update({highlight_selected_word: {enable_on_load: set_on}}); |
|
} |
|
|
|
|
|
var cm_conf = (params.code_cells_only ? CodeCell : Cell).options_default.cm_config; |
|
cm_conf.highlightSelectionMatchesInJupyterCells = cm_conf.styleSelectedText = set_on; |
|
|
|
|
|
get_relevant_cells().forEach(function (cell, idx, array) { |
|
cell.code_mirror.setOption('highlightSelectionMatchesInJupyterCells', set_on); |
|
cell.code_mirror.setOption('styleSelectedText', set_on); |
|
}); |
|
|
|
$('.' + menu_toggle_class + ' > .fa').toggleClass('fa-check', set_on); |
|
|
|
$('#site')[ |
|
(params.only_cells_in_scroll && params.scroll_min_delay > 0) ? 'on' : 'off' |
|
]('scroll', scroll_handler); |
|
console.log(log_prefix, 'toggled', set_on ? 'on' : 'off'); |
|
return set_on; |
|
} |
|
|
|
function register_new_actions () { |
|
action_names.toggle = Jupyter.keyboard_manager.actions.register({ |
|
handler : function (env) { toggle_highlight_selected(); }, |
|
help : "Toggle highlighting of selected word", |
|
icon : 'fa-language', |
|
help_index: 'c1' |
|
}, 'toggle', mod_name); |
|
} |
|
|
|
function bind_hotkeys () { |
|
if (params.use_toggle_hotkey && params.toggle_hotkey) { |
|
Jupyter.keyboard_manager.command_shortcuts.add_shortcut(params.toggle_hotkey, action_names.toggle); |
|
Jupyter.keyboard_manager.edit_shortcuts.add_shortcut(params.toggle_hotkey, action_names.toggle); |
|
} |
|
} |
|
|
|
function insert_css () { |
|
var css = [ |
|
|
|
'.edit_mode .unselected .CodeMirror .cm-matchhighlight {', |
|
' background-color: ' + params.highlight_color_blurred + ';', |
|
'}', |
|
|
|
|
|
'.edit_mode .CodeMirror.CodeMirror-focused :not(.CodeMirror-selectedtext).cm-matchhighlight {', |
|
' background-color: ' + params.highlight_color + ';', |
|
'}', |
|
|
|
|
|
'.edit_mode .CodeMirror .cm-matchhighlight-outline {', |
|
' outline-style: solid;', |
|
' outline-width: ' + params.outline_width + 'px;', |
|
' outline-color: ' + params.highlight_color_blurred + ';', |
|
'}', |
|
|
|
|
|
'.edit_mode .CodeMirror.CodeMirror-focused .cm-matchhighlight-outline {', |
|
' outline-color: ' + params.highlight_color + ';', |
|
'}' |
|
].join('\n'); |
|
|
|
if (params.hide_selections_in_unfocussed) { |
|
css += [ |
|
|
|
'.unselected .CodeMirror :not(.cm-matchhighlight).CodeMirror-selected,', |
|
'.unselected .CodeMirror :not(.cm-matchhighlight).CodeMirror-selectedtext {', |
|
' background: initial;', |
|
'}', |
|
].join('\n'); |
|
} |
|
|
|
$('<style type="text/css" id="highlight_selected_word_css">').appendTo('head').html(css); |
|
} |
|
|
|
function load_extension () { |
|
|
|
|
|
add_menu_item(); |
|
|
|
|
|
Jupyter.notebook.config.loaded |
|
.then(function () { |
|
$.extend(true, params, Jupyter.notebook.config.data.highlight_selected_word); |
|
}, function on_error (reason) { |
|
console.warn(log_prefix, 'error loading config:', reason); |
|
}) |
|
.then(insert_css) |
|
.then(function () { |
|
params.show_token = params.show_token ? new RegExp(params.show_token) : false; |
|
if (params.outlines_only) { |
|
params.highlight_style += '-outline' |
|
} |
|
|
|
toggle_highlight_selected(params.enable_on_load); |
|
|
|
register_new_actions(); |
|
bind_hotkeys(); |
|
}) |
|
|
|
.catch(function on_error (reason) { console.warn(log_prefix, 'error loading:', reason); }); |
|
} |
|
|
|
return { |
|
load_ipython_extension : load_extension |
|
}; |
|
}); |
|
|