Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new-log-viewer: Integrate Monaco Editor for enhanced log viewing. #54

Merged
merged 37 commits into from
Aug 24, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
50c928b
Add MonacoInstance.
junhaoliao Aug 17, 2024
036c077
Consolidate CSS selectors for full-screen layout config.
junhaoliao Aug 17, 2024
d694398
Add utility function to get map key by value.
junhaoliao Aug 17, 2024
c6585c7
Add CONFIG_DEFAULT to export list in config.ts.
junhaoliao Aug 17, 2024
ec315b5
Add read-only editor which wraps around MonacoEditor.
junhaoliao Aug 17, 2024
0f65c6a
Integrate <MonacoInstance/> into <Layout/>.
junhaoliao Aug 17, 2024
d4ea4c1
Updated the code to use the `goToPositionAndCenter` utility function …
junhaoliao Aug 17, 2024
82f4d3e
Add docs for handleCustomAction().
junhaoliao Aug 17, 2024
6af47c8
Change FIXME to TODO: add custom observer debounce automatic layout .
junhaoliao Aug 17, 2024
32ef377
Improve docs for <MonacoEditor/>.
junhaoliao Aug 17, 2024
0f77831
Add high-level docs for non-trivial useEffect hooks in <MonacoEditor/>.
junhaoliao Aug 17, 2024
6681b70
typo
junhaoliao Aug 17, 2024
18e3ac0
Fix typo and add docs for `setupFocusOnBacktickDown()`.
junhaoliao Aug 17, 2024
e6dc4d1
Docs & Formats - Apply suggestions from code review
junhaoliao Aug 19, 2024
331986b
Rename `setupCursorExplicitPosChangeAction` -> `setupCursorExplicitPo…
junhaoliao Aug 19, 2024
39b149e
Rename `getTouchDistance()` parameters with 0-based indexing.
junhaoliao Aug 19, 2024
0f584c6
Rename `initialDistanceRef` -> `currDistanceRef`.
junhaoliao Aug 19, 2024
afc699d
Add docs + reformat code.
junhaoliao Aug 19, 2024
3c1159a
Add typings and docs for MonacoInstance callbacks.
junhaoliao Aug 19, 2024
c1247fc
Rename `MonacoEditor` -> `MonacoInstance`.
junhaoliao Aug 19, 2024
27b9e17
Rename `ACTION` -> `ACTION_NAME` for clarity.
junhaoliao Aug 19, 2024
e16e00e
Rename `keybindings` -> `keyBindings`.
junhaoliao Aug 19, 2024
bdbaffe
Rename `initMonacoEditor` -> `createMonacoEditor`.
junhaoliao Aug 19, 2024
ab2b8cc
Add period to docstring.
kirkrodrigues Aug 20, 2024
cff1c1a
Docs & Formats - Apply suggestions from code review
junhaoliao Aug 20, 2024
cd7f46d
Reorder type definitions for local grouping.
junhaoliao Aug 21, 2024
b43a25b
Rename `unsetCachedPageSize` -> `resetCachedPageSize`.
junhaoliao Aug 21, 2024
6881263
Handle errors from setConfig in Editor component.
junhaoliao Aug 21, 2024
b0ab4f4
Ensure getLastItemNumInPrevChunk correctly handles input in the first…
junhaoliao Aug 21, 2024
f79976e
Rename `getNextItemNumInNextChunk` -> `getFirstItemNumInNextChunk`.
junhaoliao Aug 21, 2024
8fdc916
Revise cursor position changes handling to account for multiline events.
junhaoliao Aug 21, 2024
0908e1a
In `logEventNum` update useEffect hook, early return if mouse is down.
junhaoliao Aug 21, 2024
cef2d99
Print error log when unable to find log event number from cursor.
junhaoliao Aug 23, 2024
18a730d
Docs & Format - Apply suggestions from code review
junhaoliao Aug 23, 2024
0968251
Rename `getMapValueByNearestKey` -> `getMapValueWithNearestLessThanOr…
junhaoliao Aug 23, 2024
bc184e0
Remove unused `isNumberInRange()`.
junhaoliao Aug 23, 2024
bf35db7
Docs & Format - Apply suggestions from code review
junhaoliao Aug 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
167 changes: 167 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,167 @@
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 an action that is triggered when the cursor position changes in a Monaco code editor.
junhaoliao marked this conversation as resolved.
Show resolved Hide resolved
*
* @param editor
* @param onCursorExplicitPosChange
*/
const setupCursorExplicitPosChangeAction = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about setupCursorExplicitPosChangeCallback (or Handler)? The term "action" is a bit overused, so I think limiting it to things related to ActionType would be clearer.

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);
});
};

/**
*
* @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 touch1
* @param touch2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

touch0 and touch1 for consistency?

* @return The Euclidean distance between the touch points.
*/
const getTouchDistance = (touch1: Touch, touch2: Touch): number => Math.sqrt(
((touch2.pageX - touch1.pageX) ** 2) + ((touch2.pageY - touch1.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;
}

junhaoliao marked this conversation as resolved.
Show resolved Hide resolved
const initialDistanceRef: {current: Nullable<number>} = {current: null};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about currDistanceRef?

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

if (2 !== e.touches.length ||
"undefined" === typeof touch0 ||
"undefined" === typeof touch1) {
kirkrodrigues marked this conversation as resolved.
Show resolved Hide resolved
return;
}

e.preventDefault();
kirkrodrigues marked this conversation as resolved.
Show resolved Hide resolved
initialDistanceRef.current = getTouchDistance(touch0, touch1);
junhaoliao marked this conversation as resolved.
Show resolved Hide resolved
}, {passive: false});

editorContainer.addEventListener("touchmove", (e) => {
const [touch0, touch1] = e.touches;
if (2 !== e.touches.length ||
"undefined" === typeof touch0 ||
"undefined" === typeof touch1 ||
null === initialDistanceRef.current) {
return;
}
e.preventDefault();
junhaoliao marked this conversation as resolved.
Show resolved Hide resolved

const newDistance = getTouchDistance(touch0, touch1);
let newZoomLevel = (newDistance > initialDistanceRef.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
);
initialDistanceRef.current = newDistance;
monaco.editor.EditorZoom.setZoomLevel(newZoomLevel);
}, {passive: false});

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

/**
* Sets up custom actions for a monaco editor.
junhaoliao marked this conversation as resolved.
Show resolved Hide resolved
*
* @param editor
* @param actions
* @param onCustomAction
*/
const setupCustomActions = (
editor: monaco.editor.IStandaloneCodeEditor,
actions: ActionType[],
onCustomAction: CustomActionCallback
) => {
actions.forEach(({action, label, keybindings}) => {
if (null === action) {
return;
}
editor.addAction({
id: action,
label: label,
keybindings: keybindings,
run: () => {
onCustomAction(editor, action);
},
});
});
};

export {
setupCursorExplicitPosChangeAction,
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%;
}
127 changes: 127 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,127 @@
import {
useEffect,
useRef,
} from "react";

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

import {ActionType} from "../../../utils/actions";
import {
CursorExplicitPosChangeCallback,
CustomActionCallback,
} from "./typings";
import {
goToPositionAndCenter,
initMonacoEditor,
} from "./utils";

import "./index.css";


interface MonacoEditorProps {
lineNum: number,
text: string,
actions: ActionType[],
beforeMount?: () => void,
beforeTextUpdate?: (editor: monaco.editor.IStandaloneCodeEditor) => void,
onCursorExplicitPosChange: CursorExplicitPosChangeCallback,
onCustomAction: CustomActionCallback,
onMount?: (editor: monaco.editor.IStandaloneCodeEditor) => void,
onTextUpdate?: (editor: monaco.editor.IStandaloneCodeEditor) => void,
}

/**
* Wraps a `monaco-editor` instance created from the DOM rendered, which accepts a variety of props
kirkrodrigues marked this conversation as resolved.
Show resolved Hide resolved
* to configure text content, custome actions, and various lifecycle hooks for interacting with
* the editor.
*
* @param props
* @param props.text
* @param props.lineNum
junhaoliao marked this conversation as resolved.
Show resolved Hide resolved
* @param props.actions
* @param props.beforeMount
* @param props.beforeTextUpdate
* @param props.onCursorExplicitPosChange
* @param props.onCustomAction
* @param props.onMount
kirkrodrigues marked this conversation as resolved.
Show resolved Hide resolved
* @param props.onTextUpdate
* @return
*/
const MonacoEditor = ({
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");
junhaoliao marked this conversation as resolved.
Show resolved Hide resolved
if (null === editorContainerRef.current) {
console.error("Unexpected unmounted editor container div element");
kirkrodrigues marked this conversation as resolved.
Show resolved Hide resolved

return () => null;
}

beforeMount?.();
editorRef.current = initMonacoEditor(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, set the updated position cursor in the editor.
junhaoliao marked this conversation as resolved.
Show resolved Hide resolved
useEffect(() => {
if (null === editorRef.current) {
return;
}
goToPositionAndCenter(editorRef.current, {lineNumber: lineNum, column: 1});
}, [lineNum]);

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

export default MonacoEditor;
23 changes: 23 additions & 0 deletions new-log-viewer/src/components/Editor/MonacoInstance/typings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Note this is located under src/components/MonacoInstance instead of src/typings because we aim to
// make MonacoInstance a reusable component for other projects.


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

import {ACTION} from "../../../utils/actions";


type CursorExplicitPosChangeCallback = (ev: monaco.editor.ICursorPositionChangedEvent) => void;
type CustomActionCallback =
(editor: monaco.editor.IStandaloneCodeEditor, action: ACTION) => void;

interface CustomMonacoEditorHandlers {
onCursorExplicitPosChange?: CursorExplicitPosChangeCallback,
onCustomAction?: CustomActionCallback,
}

export type {
CursorExplicitPosChangeCallback,
CustomActionCallback,
CustomMonacoEditorHandlers,
};
Loading