Add breakpoints to editor UI

This commit is contained in:
Nilay Majorwar 2021-12-15 14:35:04 +05:30
parent 01ba292b9f
commit c9afb3a68b
6 changed files with 186 additions and 26 deletions

View File

@ -1,12 +1,19 @@
.code-highlight {
background-color: #ffff0077;
background-color: #ffff00aa;
}
.breakpoint-glyph {
box-sizing: border-box;
padding: 4%;
padding: 6%; /* Make the dot smaller in size */
margin-top: 2px; /* Fix dot appearing slightly above baseline */
border-radius: 50%;
margin-left: 10px;
background-color: #ff5555;
background-clip: content-box;
}
.breakpoint-glyph.solid {
background-color: #ff5555 !important;
}
.breakpoint-glyph.hint {
background-color: #ff555555;
}

View File

@ -66,6 +66,7 @@ export const Mainframe = () => {
highlights={codeHighlights}
defaultValue={providerRef.current.sampleProgram}
tokensProvider={providerRef.current.editorTokensProvider}
onUpdateBreakpoints={(newPoints) => console.log(newPoints)}
/>
)}
renderRenderer={() => (

View File

@ -1,24 +1,9 @@
import React from "react";
import Editor, { useMonaco } from "@monaco-editor/react";
import monaco from "monaco-editor";
import { useEditorLanguageConfig } from "./use-editor-lang-config";
import { DocumentRange, MonacoTokensProvider } from "../../engines/types";
import { useEditorConfig } from "./use-editor-config";
// Type aliases for the Monaco editor
type EditorInstance = monaco.editor.IStandaloneCodeEditor;
/** Create Monaco decoration range object from highlights */
const createRange = (
monacoInstance: typeof monaco,
highlights: DocumentRange
) => {
const lineNum = highlights.line;
const startChar = highlights.charRange?.start || 0;
const endChar = highlights.charRange?.end || 1000;
const range = new monacoInstance.Range(lineNum, startChar, lineNum, endChar);
const isWholeLine = !highlights.charRange;
return { range, options: { isWholeLine, inlineClassName: "code-highlight" } };
};
import { createHighlightRange, EditorInstance } from "./monaco-utils";
import { useEditorBreakpoints } from "./use-editor-breakpoints";
// Interface for interacting with the editor
export interface CodeEditorRef {
@ -37,6 +22,8 @@ type Props = {
highlights?: DocumentRange;
/** Tokens provider for the language */
tokensProvider?: MonacoTokensProvider;
/** Callback to update debugging breakpoints */
onUpdateBreakpoints: (newBreakpoints: number[]) => void;
};
/**
@ -47,7 +34,16 @@ const CodeEditorComponent = (props: Props, ref: React.Ref<CodeEditorRef>) => {
const editorRef = React.useRef<EditorInstance | null>(null);
const monacoInstance = useMonaco();
const { highlights } = props;
useEditorConfig({
// Breakpoints
useEditorBreakpoints({
editor: editorRef.current,
monaco: monacoInstance,
onUpdateBreakpoints: props.onUpdateBreakpoints,
});
// Language config
useEditorLanguageConfig({
languageId: props.languageId,
tokensProvider: props.tokensProvider,
});
@ -55,7 +51,7 @@ const CodeEditorComponent = (props: Props, ref: React.Ref<CodeEditorRef>) => {
// Change editor highlights when prop changes
React.useEffect(() => {
if (!editorRef.current || !highlights) return;
const range = createRange(monacoInstance!, highlights);
const range = createHighlightRange(monacoInstance!, highlights);
const decors = editorRef.current!.deltaDecorations([], [range]);
return () => {
editorRef.current!.deltaDecorations(decors, []);
@ -77,7 +73,7 @@ const CodeEditorComponent = (props: Props, ref: React.Ref<CodeEditorRef>) => {
defaultLanguage="brainfuck"
defaultValue={props.defaultValue}
onMount={(editor) => (editorRef.current = editor)}
options={{ minimap: { enabled: false } }}
options={{ minimap: { enabled: false }, glyphMargin: true }}
/>
);
};

View File

@ -0,0 +1,41 @@
import monaco from "monaco-editor";
import { DocumentRange } from "../../engines/types";
/** Type alias for an instance of Monaco editor */
export type EditorInstance = monaco.editor.IStandaloneCodeEditor;
/** Type alias for the Monaco global */
export type MonacoInstance = typeof monaco;
/** Type alias for Monaco mouse events */
export type MonacoMouseEvent = monaco.editor.IEditorMouseEvent;
/** Type alias for Monaco mouse-leave event */
export type MonacoMouseLeaveEvent = monaco.editor.IPartialEditorMouseEvent;
/** Type alias for Monaco decoration object */
export type MonacoDecoration = monaco.editor.IModelDeltaDecoration;
/** Create Monaco decoration range object for text highlighting */
export const createHighlightRange = (
monacoInstance: MonacoInstance,
highlights: DocumentRange
): MonacoDecoration => {
const lineNum = highlights.line;
const startChar = highlights.charRange?.start || 0;
const endChar = highlights.charRange?.end || 1000;
const range = new monacoInstance.Range(lineNum, startChar, lineNum, endChar);
const isWholeLine = !highlights.charRange;
return { range, options: { isWholeLine, inlineClassName: "code-highlight" } };
};
/** Create Monaco decoration range object from highlights */
export const createBreakpointRange = (
monacoInstance: MonacoInstance,
lineNum: number,
hint?: boolean
): MonacoDecoration => {
const range = new monacoInstance.Range(lineNum, 0, lineNum, 1000);
const className = "breakpoint-glyph " + (hint ? "hint" : "solid");
return { range, options: { glyphMarginClassName: className } };
};

View File

@ -0,0 +1,115 @@
import React from "react";
import {
createBreakpointRange,
EditorInstance,
MonacoInstance,
MonacoMouseEvent,
MonacoMouseLeaveEvent,
} from "./monaco-utils";
type BreakpointsMap = { [k: number]: string[] };
type HoverBreakpoint = {
lineNum: number;
decorRanges: string[];
};
type Args = {
editor: EditorInstance | null;
monaco: MonacoInstance | null;
onUpdateBreakpoints: (newBreakpoints: number[]) => void;
};
export const useEditorBreakpoints = ({
editor,
monaco,
onUpdateBreakpoints,
}: Args) => {
const breakpoints = React.useRef<BreakpointsMap>({});
const hoverBreakpoint = React.useRef<HoverBreakpoint | null>(null);
// Mouse clicks -> add or remove breakpoint
React.useEffect(() => {
if (!editor || !monaco) return;
const disposer = editor.onMouseDown((e: MonacoMouseEvent) => {
// Check if click is in glyph display channel
const glyphMarginType = monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN;
const isGlyphMargin = e.target.type === glyphMarginType;
if (!isGlyphMargin) return;
const lineNum = e.target.position!.lineNumber;
const existingRange = breakpoints.current[lineNum];
if (existingRange) {
// Already has breakpoint - remove it
editor.deltaDecorations(existingRange, []);
delete breakpoints.current[lineNum];
} else {
// Add breakpoint to this line
const range = createBreakpointRange(monaco, lineNum);
const newRangeStr = editor.deltaDecorations([], [range]);
breakpoints.current[lineNum] = newRangeStr;
}
// Update breakpoints to parent
const bpLineNumStrs = Object.keys(breakpoints.current);
const bpLineNums = bpLineNumStrs.map((numStr) => parseInt(numStr, 10));
onUpdateBreakpoints(bpLineNums);
});
return () => disposer.dispose();
}, [editor, monaco]);
// Mouse enter -> show semi-transparent breakpoint icon
React.useEffect(() => {
if (!editor || !monaco) return;
const disposer = editor.onMouseMove((e: MonacoMouseEvent) => {
// Check if click is in glyph display channel
const glyphMarginType = monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN;
const isGlyphMargin = e.target.type === glyphMarginType;
// If mouse goes out of glyph channel...
if (!isGlyphMargin) {
if (hoverBreakpoint.current) {
editor.deltaDecorations(hoverBreakpoint.current.decorRanges, []);
hoverBreakpoint.current = null;
}
return;
}
// Check if hover is in already hinted line
const hoverLineNum = e.target.position!.lineNumber;
if (hoverLineNum === hoverBreakpoint.current?.lineNum) return;
// Add hover decoration to newly hovered line
const range = createBreakpointRange(monaco, hoverLineNum, true);
const newHoverRangeStr = editor.deltaDecorations([], [range]);
// Remove existing breakpoint hover
if (hoverBreakpoint.current)
editor.deltaDecorations(hoverBreakpoint.current.decorRanges, []);
// Set hover breakpoint state to new one
hoverBreakpoint.current = {
lineNum: hoverLineNum,
decorRanges: newHoverRangeStr,
};
// If breakpoint already on line, ignore
const lineNum = e.target.position!.lineNumber;
const existingRange = breakpoints.current[lineNum];
if (existingRange) return;
});
return () => disposer.dispose();
}, [editor, monaco]);
// Mouse leaves editor -> remove hover breakpoint hint
React.useEffect(() => {
if (!editor) return;
const disposer = editor.onMouseLeave((e: MonacoMouseLeaveEvent) => {
if (!hoverBreakpoint.current) return;
editor.deltaDecorations(hoverBreakpoint.current.decorRanges, []);
hoverBreakpoint.current = null;
});
return () => disposer.dispose();
}, [editor]);
return breakpoints;
};

View File

@ -8,7 +8,7 @@ type ConfigParams = {
};
/** Add custom language and relevant providers to Monaco */
export const useEditorConfig = (params: ConfigParams) => {
export const useEditorLanguageConfig = (params: ConfigParams) => {
const monaco = useMonaco();
React.useEffect(() => {