import { useState, useEffect, useCallback, useRef } from 'react'; export function cursorPositionToTextOffset(cursorPos: any, body: string) { if (!body) return 0; const noteLines = body.split('\n'); let pos = 0; for (let i = 0; i < noteLines.length; i++) { if (i > 0) pos++; // Need to add the newline that's been removed in the split() call above if (i === cursorPos.row) { pos += cursorPos.column; break; } else { pos += noteLines[i].length; } } return pos; } export function currentTextOffset(editor: any, body: string) { return cursorPositionToTextOffset(editor.getCursorPosition(), body); } export function rangeToTextOffsets(range: any, body: string) { return { start: cursorPositionToTextOffset(range.start, body), end: cursorPositionToTextOffset(range.end, body), }; } export function textOffsetSelection(selectionRange: any, body: string) { return selectionRange && body ? rangeToTextOffsets(selectionRange, body) : null; } export function selectedText(selectionRange: any, body: string) { const selection = textOffsetSelection(selectionRange, body); if (!selection || selection.start === selection.end) return ''; return body.substr(selection.start, selection.end - selection.start); } export function selectionRange(editor:any) { const ranges = editor.getSelection().getAllRanges(); return ranges && ranges.length ? ranges[0] : null; } export function textOffsetToCursorPosition(offset: number, body: string) { const lines = body.split('\n'); let row = 0; let currentOffset = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (currentOffset + line.length >= offset) { return { row: row, column: offset - currentOffset, }; } row++; currentOffset += line.length + 1; } return null; } function lineAtRow(body: string, row: number) { if (!body) return ''; const lines = body.split('\n'); if (row < 0 || row >= lines.length) return ''; return lines[row]; } export function selectionRangeCurrentLine(selectionRange: any, body: string) { if (!selectionRange) return ''; return lineAtRow(body, selectionRange.start.row); } export function selectionRangePreviousLine(selectionRange: any, body: string) { if (!selectionRange) return ''; return lineAtRow(body, selectionRange.start.row - 1); } export function lineLeftSpaces(line: string) { let output = ''; for (let i = 0; i < line.length; i++) { if ([' ', '\t'].indexOf(line[i]) >= 0) { output += line[i]; } else { break; } } return output; } export function usePrevious(value: any): any { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } export function useScrollHandler(editor: any, webviewRef: any, onScroll: Function) { const editorMaxScrollTop_ = useRef(0); const restoreScrollTop_ = useRef(null); const ignoreNextEditorScrollEvent_ = useRef(false); const scrollTimeoutId_ = useRef(null); // TODO: Below is not needed anymore???? // // this.editorMaxScrollTop_ = 0; // // HACK: To go around a bug in Ace editor, we first set the scroll position to 1 // // and then (in the renderer callback) to the value we actually need. The first // // operation helps clear the scroll position cache. See: // // // this.editorSetScrollTop(1); // this.restoreScrollTop_ = 0; const editorSetScrollTop = useCallback((v) => { if (!editor) return; editor.getSession().setScrollTop(v); }, [editor]); // Complicated but reliable method to get editor content height // https://github.com/ajaxorg/ace/issues/2046 const onAfterEditorRender = useCallback(() => { const r = editor.renderer; editorMaxScrollTop_.current = Math.max(0, r.layerConfig.maxHeight - r.$size.scrollerHeight); if (restoreScrollTop_.current !== null) { editorSetScrollTop(restoreScrollTop_.current); restoreScrollTop_.current = null; } }, [editor, editorSetScrollTop]); const scheduleOnScroll = useCallback((event: any) => { if (scrollTimeoutId_.current) { clearTimeout(scrollTimeoutId_.current); scrollTimeoutId_.current = null; } scrollTimeoutId_.current = setTimeout(() => { scrollTimeoutId_.current = null; onScroll(event); }, 10); }, [onScroll]); const setEditorPercentScroll = useCallback((p: number) => { ignoreNextEditorScrollEvent_.current = true; editorSetScrollTop(p * editorMaxScrollTop_.current); scheduleOnScroll({ percent: p }); }, [editorSetScrollTop, scheduleOnScroll]); const setViewerPercentScroll = useCallback((p: number) => { if (webviewRef.current) { webviewRef.current.wrappedInstance.send('setPercentScroll', p); scheduleOnScroll({ percent: p }); } }, [scheduleOnScroll]); const editor_scroll = useCallback(() => { if (ignoreNextEditorScrollEvent_.current) { ignoreNextEditorScrollEvent_.current = false; return; } const m = editorMaxScrollTop_.current; const percent = m ? editor.getSession().getScrollTop() / m : 0; setViewerPercentScroll(percent); }, [editor, setViewerPercentScroll]); const resetScroll = useCallback(() => { if (!editor) return; // Ace Editor caches scroll values, which makes // it hard to reset the scroll position, so we // need to use this hack. // https://github.com/ajaxorg/ace/issues/2195 editor.session.$scrollTop = -1; editor.session.$scrollLeft = -1; editor.renderer.scrollTop = -1; editor.renderer.scrollLeft = -1; editor.renderer.scrollBarV.scrollTop = -1; editor.renderer.scrollBarH.scrollLeft = -1; editor.session.setScrollTop(0); editor.session.setScrollLeft(0); }, [editorSetScrollTop, editor]); useEffect(() => { if (!editor) return () => {}; editor.renderer.on('afterRender', onAfterEditorRender); return () => { editor.renderer.off('afterRender', onAfterEditorRender); }; }, [editor]); return { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll }; } export function useRootWidth(dependencies:any) { const { rootRef } = dependencies; const [rootWidth, setRootWidth] = useState(0); useEffect(() => { if (!rootRef.current) return; if (rootWidth !== rootRef.current.offsetWidth) setRootWidth(rootRef.current.offsetWidth); }); return rootWidth; }