-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
new-log-viewer: Integrate Monaco Editor for enhanced log viewing. (#54)
- Loading branch information
1 parent
5c0960c
commit fefbff7
Showing
15 changed files
with
935 additions
and
25 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
178 changes: 178 additions & 0 deletions
178
new-log-viewer/src/components/Editor/MonacoInstance/actions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
4
new-log-viewer/src/components/Editor/MonacoInstance/index.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
131
new-log-viewer/src/components/Editor/MonacoInstance/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.