Skip to content

Commit

Permalink
new-log-viewer: Integrate Monaco Editor for enhanced log viewing. (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
junhaoliao authored Aug 24, 2024
1 parent 5c0960c commit fefbff7
Show file tree
Hide file tree
Showing 15 changed files with 935 additions and 25 deletions.
20 changes: 20 additions & 0 deletions new-log-viewer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions new-log-viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dependencies": {
"axios": "^1.7.2",
"dayjs": "^1.11.11",
"monaco-editor": "^0.50.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand All @@ -39,6 +40,7 @@
"eslint-import-resolver-typescript": "^3.6.1",
"html-webpack-plugin": "^5.6.0",
"mini-css-extract-plugin": "^2.9.0",
"monaco-editor-webpack-plugin": "^7.1.0",
"react-refresh": "^0.14.2",
"style-loader": "^4.0.0",
"typescript": "^5.4.5",
Expand Down
178 changes: 178 additions & 0 deletions new-log-viewer/src/components/Editor/MonacoInstance/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";

import {Nullable} from "../../../typings/common";
import {ActionType} from "../../../utils/actions";
import {clamp} from "../../../utils/math";
import type {
CursorExplicitPosChangeCallback,
CustomActionCallback,
} from "./typings";


const MIN_ZOOM_LEVEL = 1;
const MAX_ZOOM_LEVEL = 10;
const MOBILE_ZOOM_LEVEL_INCREMENT = 10;
const MOBILE_ZOOM_LEVEL_DECREMENT = 1;
const POSITION_CHANGE_DEBOUNCE_TIMEOUT_MILLIS = 50;

/**
* Sets up a callback for when the cursor position changes in the editor.
*
* @param editor
* @param onCursorExplicitPosChange
*/
const setupCursorExplicitPosChangeCallback = (
editor: monaco.editor.IStandaloneCodeEditor,
onCursorExplicitPosChange: CursorExplicitPosChangeCallback
) => {
let posChangeDebounceTimeout: Nullable<ReturnType<typeof setTimeout>> = null;

editor.onDidChangeCursorPosition((ev: monaco.editor.ICursorPositionChangedEvent) => {
// only trigger if there was an explicit change that was made by keyboard or mouse
if (monaco.editor.CursorChangeReason.Explicit !== ev.reason) {
return;
}
if (null !== posChangeDebounceTimeout) {
clearTimeout(posChangeDebounceTimeout);
}
posChangeDebounceTimeout = setTimeout(() => {
onCursorExplicitPosChange(ev);
posChangeDebounceTimeout = null;
}, POSITION_CHANGE_DEBOUNCE_TIMEOUT_MILLIS);
});
};

/**
* Sets up editor focus on `backtick` key down.
*
* @param editor
*/
const setupFocusOnBacktickDown = (editor: monaco.editor.IStandaloneCodeEditor) => {
const handleKeyDown = (e: KeyboardEvent) => {
if ("`" === e.key) {
e.stopPropagation();
e.preventDefault();
editor.focus();
}
};

window.addEventListener("keypress", handleKeyDown);
editor.onDidDispose(() => {
window.removeEventListener("keypress", handleKeyDown);
});
};

/**
* Calculates the distance between two touch points.
*
* @param touch0
* @param touch1
* @return The Euclidean distance between the touch points.
*/
const getTouchDistance = (touch0: Touch, touch1: Touch): number => Math.sqrt(
((touch1.pageX - touch0.pageX) ** 2) + ((touch1.pageY - touch0.pageY) ** 2)
);

/**
* Sets up mobile zoom functionality by calculating distance differences between two touch points.
*
* @param editor
* @param editorContainer
*/
const setupMobileZoom = (
editor: monaco.editor.IStandaloneCodeEditor,
editorContainer: HTMLElement
) => {
const editorDomNode = editor.getDomNode();
if (null === editorDomNode) {
console.error("Unexpected null returned by editor.getDomNode()");

return;
}

// NOTE:
// - We explicitly set `passive=false` for the listeners below since, on Safari, it defaults to
// `true` for touch events (whereas it defaults to `false` otherwise).
// - The "undefined" type checks below are to satisfy TypeScript.
// - We only call `e.preventDefault()` after we validate that this is a two-touch event, to
// avoid affecting other touch events.

const currDistanceRef: {current: Nullable<number>} = {current: null};
editorContainer.addEventListener("touchstart", (e) => {
const [touch0, touch1] = e.touches;

if (2 !== e.touches.length ||
"undefined" === typeof touch0 ||
"undefined" === typeof touch1) {
return;
}

e.preventDefault();

currDistanceRef.current = getTouchDistance(touch0, touch1);
}, {passive: false});

editorContainer.addEventListener("touchmove", (e) => {
const [touch0, touch1] = e.touches;

if (2 !== e.touches.length ||
"undefined" === typeof touch0 ||
"undefined" === typeof touch1 ||
null === currDistanceRef.current) {
return;
}

e.preventDefault();

const newDistance = getTouchDistance(touch0, touch1);
let newZoomLevel = (newDistance > currDistanceRef.current) ?
monaco.editor.EditorZoom.getZoomLevel() + MOBILE_ZOOM_LEVEL_INCREMENT :
monaco.editor.EditorZoom.getZoomLevel() - MOBILE_ZOOM_LEVEL_DECREMENT;

newZoomLevel = clamp(
newZoomLevel,
MIN_ZOOM_LEVEL,
MAX_ZOOM_LEVEL
);
currDistanceRef.current = newDistance;
monaco.editor.EditorZoom.setZoomLevel(newZoomLevel);
}, {passive: false});

editorContainer.addEventListener("touchend", () => {
currDistanceRef.current = null;
});
};

/**
* Sets up custom actions for the editor.
*
* @param editor
* @param actions
* @param onCustomAction
*/
const setupCustomActions = (
editor: monaco.editor.IStandaloneCodeEditor,
actions: ActionType[],
onCustomAction: CustomActionCallback
) => {
actions.forEach(({actionName, label, keyBindings}) => {
if (null === actionName) {
return;
}
editor.addAction({
id: actionName,
label: label,
keybindings: keyBindings,
run: () => {
onCustomAction(editor, actionName);
},
});
});
};

export {
setupCursorExplicitPosChangeCallback,
setupCustomActions,
setupFocusOnBacktickDown,
setupMobileZoom,
};
4 changes: 4 additions & 0 deletions new-log-viewer/src/components/Editor/MonacoInstance/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.monaco-container {
width: 100%;
height: 100%;
}
131 changes: 131 additions & 0 deletions new-log-viewer/src/components/Editor/MonacoInstance/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {
useEffect,
useRef,
} from "react";

import * as monaco from "monaco-editor/esm/vs/editor/editor.api";

import {ActionType} from "../../../utils/actions";
import {
BeforeMountCallback,
BeforeTextUpdateCallback,
CursorExplicitPosChangeCallback,
CustomActionCallback,
MountCallback,
TextUpdateCallback,
} from "./typings";
import {
createMonacoEditor,
goToPositionAndCenter,
} from "./utils";

import "./index.css";


interface MonacoEditorProps {
lineNum: number,
text: string,
actions: ActionType[],
beforeMount?: BeforeMountCallback,
beforeTextUpdate?: BeforeTextUpdateCallback,
onCursorExplicitPosChange: CursorExplicitPosChangeCallback,
onCustomAction: CustomActionCallback,
onMount?: MountCallback,
onTextUpdate?: TextUpdateCallback,
}

/**
* Wraps a `monaco-editor` instance for viewing text content. The component accepts a variety of
* props to configure the content, custom actions, and various lifecycle hooks for interacting with
* the editor.
*
* @param props
* @param props.lineNum
* @param props.text
* @param props.actions
* @param props.beforeMount
* @param props.beforeTextUpdate
* @param props.onCursorExplicitPosChange
* @param props.onCustomAction
* @param props.onMount
* @param props.onTextUpdate
* @return
*/
const MonacoInstance = ({
lineNum,
text,
actions,
beforeMount,
beforeTextUpdate,
onMount,
onCursorExplicitPosChange,
onCustomAction,
onTextUpdate,
}: MonacoEditorProps) => {
const editorRef = useRef<null|monaco.editor.IStandaloneCodeEditor>(null);
const editorContainerRef = useRef<HTMLDivElement>(null);
const lineNumRef = useRef<number>(lineNum);

// Synchronize `lineNumRef` with `lineNum`.
useEffect(() => {
lineNumRef.current = lineNum;
}, [lineNum]);

useEffect(() => {
console.log("Initiating Monaco instance");
if (null === editorContainerRef.current) {
console.error("Unexpected unmounted editor container div element");

return () => null;
}

beforeMount?.();
editorRef.current = createMonacoEditor(editorContainerRef.current, actions, {
onCursorExplicitPosChange: onCursorExplicitPosChange,
onCustomAction: onCustomAction,
});
onMount?.(editorRef.current);

return () => {
editorRef.current?.dispose();
editorRef.current = null;
};
}, [
actions,
beforeMount,
onCursorExplicitPosChange,
onCustomAction,
onMount,
]);

// On `text` update, set the text and position cursor in the editor.
useEffect(() => {
if (null === editorRef.current) {
return;
}
beforeTextUpdate?.(editorRef.current);
editorRef.current.setValue(text);
goToPositionAndCenter(editorRef.current, {lineNumber: lineNumRef.current, column: 1});
onTextUpdate?.(editorRef.current);
}, [
text,
beforeTextUpdate,
onTextUpdate,
]);

// On `lineNum` update, update the cursor's position in the editor.
useEffect(() => {
if (null === editorRef.current) {
return;
}
goToPositionAndCenter(editorRef.current, {lineNumber: lineNum, column: 1});
}, [lineNum]);

return (
<div
className={"monaco-container"}
ref={editorContainerRef}/>
);
};

export default MonacoInstance;
Loading

0 comments on commit fefbff7

Please sign in to comment.