/** Imports */ import * as commands from "../../commands/commands"; import * as React from "react"; import * as ReactDOM from "react-dom"; import * as utils from "../../../common/utils"; import * as events from "../../../common/events"; import * as monacoUtils from "../monacoUtils"; /** Load jumpy css */ require('./jumpy.css'); /** Editor type */ type Editor = monaco.editor.ICodeEditor; import Range = monaco.Range; /** * Where we store out state */ declare global { namespace monaco { namespace editor { export interface ICommonCodeEditor { _jumpyState?: JumpyState | null; } } } } /** * Setup key characters used for jumpy points */ let lowerCharacters = []; for (let i = 'a'.charCodeAt(0); i <= 'z'.charCodeAt(0); i++) { const key = String.fromCharCode(i) lowerCharacters.push(key); } export let keys: string[] = [] for (let c1 of lowerCharacters) { for (let c2 of lowerCharacters) { keys.push(c1 + c2); } } /** information about a jump point */ interface JumpyWidget { node: HTMLDivElement; line: number; ch: number; keys: string; monacoWiget: monaco.editor.IContentWidget | null; } function getWidgetId(wg: JumpyWidget) { return `jumpy:${wg.line}:${wg.ch}`; } /** needs no comment */ interface JumpyState { shown: boolean; disposible: events.CompositeDisposible; widgets?: JumpyWidget[]; key1?: string; key2?: string; } /** Gets or creates a state */ export function getState(editor: Editor): JumpyState { return editor._jumpyState || (editor._jumpyState = { widgets: [], shown: false, disposible: new events.CompositeDisposible() }); } /** * * The bulk of the logic * */ function addOverlay(editor: Editor) { clearAnyOverlay(editor); /** Create the overlays */ const state = createOverlays(editor); /** A clear overlay function just for this editor 🌹 */ const clearOverlay = () => clearAnyOverlay(editor); // Subscribe to esc to clear state.disposible.add(commands.esc.on(clearOverlay)); /** Subscribe to jump */ state.disposible.add(editor.onKeyDown((e: monaco.IKeyboardEvent) => { if (e.keyCode >= monaco.KeyCode.KEY_A && e.keyCode <= monaco.KeyCode.KEY_Z) { // We always prevent ascii chars as the user might have mistyped some keystroke e.preventDefault(); e.stopPropagation(); // The character representation const char = String.fromCharCode(e.browserEvent.which).toLowerCase(); // console.log({char}); // DEBUG let state = getState(editor); if (!state.key1) { state.key1 = char; // remove not matched state.widgets.filter(wg=>!wg.keys.startsWith(state.key1)).forEach(wg => editor.removeContentWidget(wg.monacoWiget)); // only keep matched state.widgets = state.widgets.filter(wg=>wg.keys.startsWith(state.key1)); // remove all if nothing matched if (state.widgets.length == 0){ clearOverlay(); } } else { let total = state.key1 + char; let matched = state.widgets.find(wg=>wg.keys == total); if (matched) { const position = {lineNumber: matched.line + 1, column: matched.ch + 1}; editor.setPosition(position); // We actually allow them to jump one line before / one line after // So calling this isn't a bad idea. No-op if the line is in view ;) editor.revealLine(position.lineNumber); } clearOverlay(); } } else { clearOverlay(); } })); /** Best to exit on these conditions too */ state.disposible.add(editor.onDidScrollChange(clearOverlay)); state.disposible.add(editor.onDidChangeCursorSelection(clearOverlay)); state.disposible.add(editor.onDidBlurEditor(clearOverlay)); } /** * Clears previous overlays if any */ function clearAnyOverlay(editor: Editor) { let state = getState(editor); if (state.shown) { state.widgets.forEach(wg => editor.removeContentWidget(wg.monacoWiget)); state.widgets = []; state.key1 = null; state.key2 = null; state.shown = false; state.disposible.dispose(); } } /** * Renders the overlays on the editor */ function createOverlays(editor: Editor) { // The model let doc = editor.getModel(); // DEBUG // console.log(editor); // window.foo = editor; // The text in viewport const range: Range = monacoUtils.getVisibleLines(editor); let text = doc.getValueInRange(range); /** What we use to split the text */ let splitRegex = /^[A-Z]?[0-9a-z]+|^[\{\};]+/; let lineNumbers:number[] = []; for (let i = range.startLineNumber; i <= range.endLineNumber; i++) { lineNumbers.push(i); } // keeps track of the next jump point key we can use let keysIndex = 0; let overlayByLines = utils.selectMany(lineNumbers.map((lineNumber, i) => { const string = doc.getLineContent(lineNumber); let pos = 0; let lineOverlays: JumpyWidget[] = []; while (pos < string.length) { var matches = splitRegex.exec(string.substr(pos)); if (matches && matches.length) { let matched = matches[0]; let name = keys[keysIndex++]; let nodeRendered =