codemirror-graphql
Version:
GraphQL mode and helpers for CodeMirror.
161 lines (133 loc) • 4.26 kB
text/typescript
/**
* Copyright (c) 2021 GraphQL Contributors
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
import CodeMirror from 'codemirror';
import { GraphQLJumpOptions } from '../jump';
CodeMirror.defineOption(
'jump',
false,
(
cm: CodeMirror.Editor,
options: GraphQLJumpOptions,
old?: GraphQLJumpOptions,
) => {
if (old && old !== CodeMirror.Init) {
const oldOnMouseOver = cm.state.jump.onMouseOver;
CodeMirror.off(cm.getWrapperElement(), 'mouseover', oldOnMouseOver);
const oldOnMouseOut = cm.state.jump.onMouseOut;
CodeMirror.off(cm.getWrapperElement(), 'mouseout', oldOnMouseOut);
CodeMirror.off(document, 'keydown', cm.state.jump.onKeyDown);
delete cm.state.jump;
}
if (options) {
const state = (cm.state.jump = {
options,
onMouseOver: onMouseOver.bind(null, cm),
onMouseOut: onMouseOut.bind(null, cm),
onKeyDown: onKeyDown.bind(null, cm),
});
CodeMirror.on(cm.getWrapperElement(), 'mouseover', state.onMouseOver);
CodeMirror.on(cm.getWrapperElement(), 'mouseout', state.onMouseOut);
CodeMirror.on(document, 'keydown', state.onKeyDown);
}
},
);
function onMouseOver(cm: CodeMirror.Editor, event: MouseEvent) {
const target = event.target || event.srcElement;
if (!(target instanceof HTMLElement)) {
return;
}
if (target?.nodeName !== 'SPAN') {
return;
}
const box = target.getBoundingClientRect();
const cursor = {
left: (box.left + box.right) / 2,
top: (box.top + box.bottom) / 2,
};
cm.state.jump.cursor = cursor;
if (cm.state.jump.isHoldingModifier) {
enableJumpMode(cm);
}
}
function onMouseOut(cm: CodeMirror.Editor) {
if (!cm.state.jump.isHoldingModifier && cm.state.jump.cursor) {
cm.state.jump.cursor = null;
return;
}
if (cm.state.jump.isHoldingModifier && cm.state.jump.marker) {
disableJumpMode(cm);
}
}
function onKeyDown(cm: CodeMirror.Editor, event: KeyboardEvent) {
if (cm.state.jump.isHoldingModifier || !isJumpModifier(event.key)) {
return;
}
cm.state.jump.isHoldingModifier = true;
if (cm.state.jump.cursor) {
enableJumpMode(cm);
}
const onKeyUp = (upEvent: KeyboardEvent) => {
if (upEvent.code !== event.code) {
return;
}
cm.state.jump.isHoldingModifier = false;
if (cm.state.jump.marker) {
disableJumpMode(cm);
}
CodeMirror.off(document, 'keyup', onKeyUp);
CodeMirror.off(document, 'click', onClick);
cm.off('mousedown', onMouseDown);
};
const onClick = (clickEvent: MouseEvent) => {
const { destination, options } = cm.state.jump;
if (destination) {
options.onClick(destination, clickEvent);
}
};
const onMouseDown = (_: any, downEvent: MouseEvent) => {
if (cm.state.jump.destination) {
(downEvent as any).codemirrorIgnore = true;
}
};
CodeMirror.on(document, 'keyup', onKeyUp);
CodeMirror.on(document, 'click', onClick);
cm.on('mousedown', onMouseDown);
}
const isMac =
typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac');
function isJumpModifier(key: string) {
return key === (isMac ? 'Meta' : 'Control');
}
function enableJumpMode(cm: CodeMirror.Editor) {
if (cm.state.jump.marker) {
return;
}
const { cursor, options } = cm.state.jump;
const pos = cm.coordsChar(cursor);
const token = cm.getTokenAt(pos, true);
const getDestination = options.getDestination || cm.getHelper(pos, 'jump');
if (getDestination) {
const destination = getDestination(token, options, cm);
if (destination) {
const marker = cm.markText(
{ line: pos.line, ch: token.start },
{ line: pos.line, ch: token.end },
{ className: 'CodeMirror-jump-token' },
);
cm.state.jump.marker = marker;
cm.state.jump.destination = destination;
}
}
}
function disableJumpMode(cm: CodeMirror.Editor) {
const { marker } = cm.state.jump;
cm.state.jump.marker = null;
cm.state.jump.destination = null;
marker.clear();
}