private Task SetBreakpointInDNA(PossibleBreakpointLocation breakpointLocation)
        {
            var jsExpression = $@"(function() {{
                if (window._dnaRuntimeHasStarted) {{
                    setBreakpointInDna('{breakpointLocation.DnaMethodIdentifier}', {breakpointLocation.SequencePointIndex});
                }} else {{
                    // Can't rely on any script files being loaded yet, so just write to global state
                    window._pendingBreakpoints = window._pendingBreakpoints || [];
                    window._pendingBreakpoints.push({{ dnaMethodId: '{breakpointLocation.DnaMethodIdentifier}', ilOffset: {breakpointLocation.SequencePointIndex} }});
                }}
            }})()";

            return(EvaluateJsInBrowser(jsExpression));
        }
        private async Task HandleMessageFromBrowser(MessageBase message)
        {
            switch (message.Method)
            {
            case "Debugger.paused":
            {
                var callFrames   = message.Params.GetValue("callFrames").Value <JArray>();
                var topCallFrame = callFrames.FirstOrDefault()?.Value <JObject>();
                var functionName = topCallFrame?.GetValue("functionName").Value <string>();
                if (functionName == "SendDebuggerMessage")
                {
                    await NotifyIdeAboutDotnetBreakpointHit(message, topCallFrame);

                    return;
                }
                break;
            }

            case "Debugger.resumed":
            {
                _currentlyPausedInDotNetBreakpoint = null;
                break;
            }

            case "Runtime.executionContextCreated":
            {
                var context = message.Params.GetValue("context").Value <JObject>();
                var auxData = context.GetValue("auxData").Value <JObject>();
                var isDefaultExecutionContext = auxData.GetValue("isDefault").Value <bool>();
                if (isDefaultExecutionContext)
                {
                    var contextId = context.GetValue("id").Value <int>();
                    await OnDefaultExecutionContextCreated(contextId, auxData);
                }
                break;
            }
            }

            // Temporarily, just proxy everything to the IDE
            await _ideConnection.SendMessageAsync(message);
        }
        private async Task NotifyIdeAboutDotnetBreakpointHit(MessageBase message, JObject nativeTopCallFrame)
        {
            var nativeCallFrameId = nativeTopCallFrame.GetValue("callFrameId").Value <string>();
            var debuggerMessage   = await EvaluateJsInBrowser("message", nativeCallFrameId);

            var debuggerMessageJson   = debuggerMessage.GetValue("value").Value <string>();
            var debuggerMessageParsed = JsonConvert.DeserializeObject <JObject>(debuggerMessageJson);
            var dnaLocationId         = debuggerMessageParsed.GetValue("ID").Value <string>();
            var ilOffset     = debuggerMessageParsed.GetValue("ilOffset").Value <int>();
            var breakpointId = CreateBreakpointId(dnaLocationId, ilOffset);

            // Modify the message to indicate we hit the .NET breakpoint, then pass it through to
            // the IDE. TODO: Also fix up the call stack, etc.
            var breakpoint = _debugInfoStore.FindBreakpointUsingDnaData(dnaLocationId, ilOffset, out var sourceFile);

            if (breakpoint == null)
            {
                Console.WriteLine($"Could not find .NET breakpoint corresponding to IL offset {ilOffset} in ID {dnaLocationId}");
                await _ideConnection.SendMessageAsync(message);

                return;
            }
            _currentlyPausedInDotNetBreakpoint = breakpoint;

            var lineNumber = breakpoint.SequencePointInfo.StartLine;
            var colNumber  = breakpoint.SequencePointInfo.StartColumn;

            Console.WriteLine($"Hit .NET breakpoint at {sourceFile.FileName} line {lineNumber} col {colNumber}\n");

            var nativeCallFrames = message.Params.GetValue("callFrames").Values <JObject>();
            var dotNetCallFrames = new[]
            {
                // TODO: Include all .NET call frames, not just the top one
                DebuggerConnection.ToJObject(new
                {
                    CallFrameId      = "dotnetcallframe:0",
                    FunctionName     = breakpoint.MethodName,
                    FunctionLocation = new {
                        ScriptId     = sourceFile.Id,
                        LineNumber   = lineNumber - 1, // TODO: Get actual line/col where the function starts, not the breakpoint hit
                        ColumnNumber = colNumber - 1
                    },
                    Location = new {
                        ScriptId     = sourceFile.Id,
                        LineNumber   = lineNumber - 1,
                        ColumnNumber = colNumber - 1
                    },
                    ScopeChain = EmptyJObject, // TODO: Populate, so it can show locals
                    This       = EmptyJObject
                })
            };
            await _ideConnection.SendMessageAsync(new MessageBase
            {
                Method = "Debugger.paused",
                Params = DebuggerConnection.ToJObject(new
                {
                    CallFrames     = dotNetCallFrames.Concat(nativeCallFrames),
                    Reason         = "other",
                    HitBreakpoints = new[] { breakpointId }
                })
            });
        }