UNPKG

@krassowski/jupyterlab_go_to_definition

Version:

Jump to definition of a variable or function in JupyterLab

151 lines 6.31 kB
import CodeMirror from 'codemirror'; import { CodeMirrorTokensProvider } from './tokens'; const HANDLERS_ON = '_go_to_are_handlers_on'; function getModifierState(event, modifierKey) { // Note: Safari does not support getModifierState on MouseEvent, see: // https://github.com/krassowski/jupyterlab-go-to-definition/issues/3 // thus AltGraph and others are not supported on Safari // Full list of modifier keys and mappings to physical keys on different OSes: // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState if (event.getModifierState !== undefined) { return event.getModifierState(modifierKey); } switch (modifierKey) { case 'Shift': return event.shiftKey; case 'Alt': return event.altKey; case 'Control': return event.ctrlKey; case 'Meta': return event.metaKey; default: console.warn(`unknown modified key ${modifierKey}`); } } export class CodeMirrorExtension extends CodeMirrorTokensProvider { constructor(editor, jumper) { super(editor); this.jumper = jumper; } static configure() { // this option is used as a flag to determine if an instance of CodeMirror // has been assigned with a handler CodeMirror.defineOption(HANDLERS_ON, false, () => { // nothing here yet }); } connect() { let editor = this.editor.editor; if (editor.getOption(HANDLERS_ON)) { // this editor instance already has the event handler return; } editor.setOption(HANDLERS_ON, true); CodeMirror.on(editor, 'mousedown', (editor, event) => { // codemirror_editor.addKeydownHandler() let target = event.target; const { button } = event; if (button === 0 && getModifierState(event, CodeMirrorExtension.modifierKey)) { const classes = ['cm-variable', 'cm-property']; if (classes.indexOf(target.className) !== -1) { let lookupName = target.textContent; let token = this.selectToken(lookupName, target); this.jumper.jump_to_definition({ token: token, mouseEvent: event, origin: target }); } event.preventDefault(); event.stopPropagation(); } }); } selectToken(lookupName, target) { // Offset is needed to handle same-cell jumping. // To get offset we could either derive it from the DOM // or from the tokens. Tokens sound better, but there is // no direct link between DOM and tokens. // This can be worked around using: // CodeMirror.display.renderView.measure.map // (see: https://stackoverflow.com/a/35937312/6646912) // or by simply counting the number of tokens before. // For completeness - using cursor does not work reliably: // const cursor = this.getCursorPosition(); // const token = this.getTokenForPosition(cursor); let cellTokens = this.editor.getTokens(); let typeFilterOn = target.className.includes('cm-variable') || target.className.includes('cm-property'); let lookupType = target.className.indexOf('cm-variable') !== -1 ? 'variable' : 'property'; let classFilter = 'cm-' + lookupType; let usagesBeforeTarget = CodeMirrorExtension._countUsagesBefore(lookupName, target, classFilter, typeFilterOn); // select relevant token let token = null; let matchedTokensCount = 0; for (let j = 0; j < cellTokens.length; j++) { let testedToken = cellTokens[j]; if (testedToken.value === lookupName && (!typeFilterOn || lookupType === testedToken.type)) { matchedTokensCount += 1; if (matchedTokensCount - 1 === usagesBeforeTarget) { token = testedToken; break; } } } // verify token if (token.value !== lookupName) { console.error(`Token ${token.value} does not match element ${lookupName}`); // fallback token = { value: lookupName, offset: 0, type: lookupType }; } return token; } static _countUsagesBefore(lookupName, target, classFilter, classFilterOn) { // count tokens with same value that occur before // (not all the tokens - to reduce the hurdle of // mapping DOM into tokens) let usagesBeforeTarget = -1; let sibling = target; const root = sibling.getRootNode(); // usually should not exceed that level, but to prevent huge files from trashing the UI... let max_iter = 10000; function stop_condition(node) { return (!node || (node.nodeType === 1 && node.className.includes('CodeMirror-lines'))); } while (!stop_condition(sibling) && !sibling.isEqualNode(root) && max_iter) { if (sibling.textContent === lookupName && (!classFilterOn || (sibling.nodeType === 1 && sibling.className.includes(classFilter)))) { usagesBeforeTarget += 1; } let nextSibling = sibling.previousSibling; while (nextSibling == null) { while (!sibling.previousSibling) { sibling = sibling.parentNode; if (stop_condition(sibling)) { return usagesBeforeTarget; } } sibling = sibling.previousSibling; while (sibling.lastChild && sibling.textContent !== lookupName) { sibling = sibling.lastChild; } nextSibling = sibling; } sibling = nextSibling; max_iter -= 1; } return usagesBeforeTarget; } } //# sourceMappingURL=extension.js.map