<commit_before>/*
 * navigation.ts
 *
 * Copyright (C) 2020 by RStudio, PBC
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

import { EditorView } from 'prosemirror-view';

import { setTextSelection, Predicate, findChildren, findDomRefAtPos } from 'prosemirror-utils';

import zenscroll from 'zenscroll';

import { editingRootNode } from './node';
import { kNavigationTransaction } from './transaction';
import { xrefPosition } from './xref';

export interface EditorNavigation {
  navigate: (type: NavigationType, location: string, animate?: boolean) => void;
}

export enum NavigationType {
  Pos = "pos",
  Id = "id",
  Href = "href",
  Heading = "heading",
  XRef = "xref"
}

export interface Navigation {
  pos: number;
  prevPos: number;
}

export function navigateTo(view: EditorView, type: NavigationType, location: string, animate = true): Navigation | null {

  switch (type) {
    case NavigationType.Pos:
      return navigateToPos(view, parseInt(location, 10), animate);
    case NavigationType.Id:
      return navigateToId(view, location, animate);
    case NavigationType.Href:
      return navigateToHref(view, location, animate);
    case NavigationType.Heading:
      return navigateToHeading(view, location, animate);
    case NavigationType.XRef:
      return navigateToXRef(view, location, animate);
    default:
      return null;
  }
}

export function navigateToId(view: EditorView, id: string, animate = true): Navigation | null {
  return navigate(view, node => id === node.attrs.navigation_id, animate);
}

export function navigateToHref(view: EditorView, href: string, animate = true): Navigation | null {
  return navigate(view, node => node.attrs.id === href, animate);
}

export function navigateToHeading(view: EditorView, heading: string, animate = true): Navigation | null {
  return navigate(
    view,
    node => {
      return (
        node.type === view.state.schema.nodes.heading &&
        node.textContent.localeCompare(heading, undefined, { sensitivity: 'accent' }) === 0
      );
    },
    animate,
  );
}

export function navigateToXRef(view: EditorView, xref: string, animate = true): Navigation | null {
  const xrefPos = xrefPosition(view.state.doc, xref);
  if (xrefPos !== -1) {
    return navigateToPos(view, xrefPos, animate);
  } else {
    return null;
  }
}


export function navigateToPos(view: EditorView, pos: number, animate = true): Navigation | null {

  // get previous position
  const prevPos = view.state.selection.from;

  // need to target at least the body
  pos = Math.max(pos, 2);

  // set selection
  const tr = view.state.tr;
  setTextSelection(pos)(tr);
  tr.setMeta(kNavigationTransaction, true);
  view.dispatch(tr);

  // find a targetable dom node at the position
  const node = findDomRefAtPos(pos, view.domAtPos.bind(view));
  if (node instanceof HTMLElement) {

    // auto-scroll to position (delay so we can grab the focus, as autoscrolling
    // doesn't seem to work unless you have the focus)
    setTimeout(() => {
      view.focus();
      const editingRoot = editingRootNode(view.state.selection)!;
      const container = view.nodeDOM(editingRoot.pos) as HTMLElement;
      const scroller = zenscroll.createScroller(container, 700, 20);
      if (animate) {
        scroller.to(node);
      } else {
        scroller.to(node, 0);
      }
    }, 200);

    return { pos, prevPos };

  } else {
    return null;
  }
}

function navigate(view: EditorView, predicate: Predicate, animate = true): Navigation | null {
  const result = findChildren(view.state.doc, predicate);
  if (result.length) {
    // pos points immediately before the node so add 1 to it
    const pos = result[0].pos + 1;
    return navigateToPos(view, pos, animate);
  } else {
    return null;
  }
}
<commit_msg>make scrolling to unrendered editors work<commit_after>/*
 * navigation.ts
 *
 * Copyright (C) 2020 by RStudio, PBC
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

import { EditorView } from 'prosemirror-view';

import { setTextSelection, Predicate, findChildren, findDomRefAtPos } from 'prosemirror-utils';

import zenscroll from 'zenscroll';

import { editingRootNode } from './node';
import { kNavigationTransaction } from './transaction';
import { xrefPosition } from './xref';

export interface EditorNavigation {
  navigate: (type: NavigationType, location: string, animate?: boolean) => void;
}

export enum NavigationType {
  Pos = "pos",
  Id = "id",
  Href = "href",
  Heading = "heading",
  XRef = "xref"
}

export interface Navigation {
  pos: number;
  prevPos: number;
}

export function navigateTo(view: EditorView, type: NavigationType, location: string, animate = true): Navigation | null {

  switch (type) {
    case NavigationType.Pos:
      return navigateToPos(view, parseInt(location, 10), animate);
    case NavigationType.Id:
      return navigateToId(view, location, animate);
    case NavigationType.Href:
      return navigateToHref(view, location, animate);
    case NavigationType.Heading:
      return navigateToHeading(view, location, animate);
    case NavigationType.XRef:
      return navigateToXRef(view, location, animate);
    default:
      return null;
  }
}

export function navigateToId(view: EditorView, id: string, animate = true): Navigation | null {
  return navigate(view, node => id === node.attrs.navigation_id, animate);
}

export function navigateToHref(view: EditorView, href: string, animate = true): Navigation | null {
  return navigate(view, node => node.attrs.id === href, animate);
}

export function navigateToHeading(view: EditorView, heading: string, animate = true): Navigation | null {
  return navigate(
    view,
    node => {
      return (
        node.type === view.state.schema.nodes.heading &&
        node.textContent.localeCompare(heading, undefined, { sensitivity: 'accent' }) === 0
      );
    },
    animate,
  );
}

export function navigateToXRef(view: EditorView, xref: string, animate = true): Navigation | null {
  const xrefPos = xrefPosition(view.state.doc, xref);
  if (xrefPos !== -1) {
    return navigateToPos(view, xrefPos, animate);
  } else {
    return null;
  }
}


export function navigateToPos(view: EditorView, pos: number, animate = true): Navigation | null {

  // get previous position
  const prevPos = view.state.selection.from;

  // need to target at least the body
  pos = Math.max(pos, 2);

  // set selection
  const tr = view.state.tr;
  setTextSelection(pos)(tr);
  tr.setMeta(kNavigationTransaction, true);
  view.dispatch(tr);

  // find a targetable dom node at the position
  const node = findDomRefAtPos(pos, view.domAtPos.bind(view));
  if (node instanceof HTMLElement) {

    // auto-scroll to position (delay so we can grab the focus, as autoscrolling
    // doesn't seem to work unless you have the focus)
    setTimeout(() => {
      view.focus();
      const editingRoot = editingRootNode(view.state.selection)!;
      const container = view.nodeDOM(editingRoot.pos) as HTMLElement;
      const scroller = zenscroll.createScroller(container, 700, 20);
      // some nodes' DOM elements are grandchildren rather than direct children
      // of the scroll container; move up a level if this is the case
      let dest = node;
      if (node.parentElement && 
          node.parentElement.parentElement &&
          node.parentElement.parentElement.parentElement === container) {
         dest = node.parentElement;
      }
      if (animate) {
        scroller.to(dest);
      } else {
        scroller.to(dest, 0);
      }
    }, 200);

    return { pos, prevPos };

  } else {
    return null;
  }
}

function navigate(view: EditorView, predicate: Predicate, animate = true): Navigation | null {
  const result = findChildren(view.state.doc, predicate);
  if (result.length) {
    // pos points immediately before the node so add 1 to it
    const pos = result[0].pos + 1;
    return navigateToPos(view, pos, animate);
  } else {
    return null;
  }
}

{'fraction_non_alphanumeric': 0.10948411804460464, 'fraction_numerical': 0.005406623113313809, 'mean_word_length': 3.541687979539642, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6714171', 'n_tokens_mistral': 2705, 'n_tokens_neox': 2555, 'n_words': 838}