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

Add navigation support to the object inspector #5206

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/devtools_app/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,9 @@ List<DevToolsScreen> get defaultScreens {
),
DevToolsScreen<VMDeveloperToolsController>(
VMDeveloperToolsScreen(),
createController: (_) => VMDeveloperToolsController(),
createController: (routerDelegate) => VMDeveloperToolsController(
routerDelegate: routerDelegate,
),
),
// Show the sample DevTools screen.
if (debugEnableSampleScreen && (kDebugMode || kProfileMode))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:vm_service/vm_service.dart';

Expand Down Expand Up @@ -35,7 +37,9 @@ class ClassHierarchyExplorer extends StatelessWidget {
controller.classHierarchyController.selectedIsolateClassHierarchy,
dataDisplayProvider: (node, onPressed) => VmServiceObjectLink<Class>(
object: node.cls,
onTap: controller.findAndSelectNodeForObject,
onTap: (cls) => unawaited(
controller.findAndSelectNodeForObject(context, cls),
bkonyi marked this conversation as resolved.
Show resolved Hide resolved
),
),
onItemSelected: (_) => null,
emptyTreeViewBuilder: () => const CenteredCircularProgressIndicator(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../../../shared/analytics/constants.dart' as gac;
import '../../../shared/globals.dart';
import '../../../shared/primitives/auto_dispose.dart';
import '../../../shared/split.dart';
import '../../../shared/ui/tab.dart';
import '../../debugger/program_explorer.dart';
Expand Down Expand Up @@ -43,16 +45,26 @@ class _ObjectInspectorView extends StatefulWidget {
}

class _ObjectInspectorViewState extends State<_ObjectInspectorView>
with TickerProviderStateMixin {
with TickerProviderStateMixin, AutoDisposeMixin {
late ObjectInspectorViewController controller;

@override
void initState() {
super.initState();

addAutoDisposeListener(
scriptManager.sortedScripts,
() => controller.initializeForCurrentIsolate(context),
);
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
final vmDeveloperToolsController =
Provider.of<VMDeveloperToolsController>(context);
controller = vmDeveloperToolsController.objectInspectorViewController
..init();
..init(context);
}

@override
Expand Down Expand Up @@ -85,7 +97,9 @@ class _ObjectInspectorViewState extends State<_ObjectInspectorView>
),
ObjectStoreViewer(
controller: controller.objectStoreController,
onLinkTapped: controller.findAndSelectNodeForObject,
onLinkTapped: (objRef) => unawaited(
controller.findAndSelectNodeForObject(context, objRef),
),
),
ClassHierarchyExplorer(
controller: controller,
Expand All @@ -104,7 +118,13 @@ class _ObjectInspectorViewState extends State<_ObjectInspectorView>
final location = node.location;
if (objRef != null &&
objRef != controller.objectHistory.current.value?.ref) {
unawaited(controller.pushObject(objRef, scriptRef: location?.scriptRef));
unawaited(
controller.pushObject(
context,
objRef,
scriptRef: location?.scriptRef,
),
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';

import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:vm_service/vm_service.dart';

import '../../../shared/globals.dart';
import '../../../shared/primitives/auto_dispose.dart';
import '../../../shared/routing.dart';
import '../../debugger/codeview_controller.dart';
import '../../debugger/program_explorer_controller.dart';
import 'class_hierarchy_explorer_controller.dart';
Expand All @@ -18,16 +23,11 @@ import 'vm_object_model.dart';
/// Stores the state information for the object inspector view related to
/// the object history and the object viewport.
class ObjectInspectorViewController extends DisposableController
with AutoDisposeControllerMixin {
with AutoDisposeControllerMixin, RouteStateHandlerMixin {
ObjectInspectorViewController({
ClassHierarchyExplorerController? classHierarchyController,
}) : classHierarchyController =
classHierarchyController ?? ClassHierarchyExplorerController() {
addAutoDisposeListener(
scriptManager.sortedScripts,
_initializeForCurrentIsolate,
);

addAutoDisposeListener(
objectHistory.current,
_onCurrentObjectChanged,
Expand All @@ -50,16 +50,47 @@ class ObjectInspectorViewController extends DisposableController

bool _initialized = false;

void init() {
void init(BuildContext context) {
if (!_initialized) {
programExplorerController
..initialize()
..initListeners();
_initializeForCurrentIsolate();
initializeForCurrentIsolate(context);
_initialized = true;
}
}

@override
void onRouteStateUpdate(DevToolsNavigationState state) {
switch (state.kind) {
case ObjectInspectorNavigationState.type:
_handleNavigationEvent(state);
break;
}
}

void _handleNavigationEvent(DevToolsNavigationState state) async {
final processedState = ObjectInspectorNavigationState._fromState(state);
final objRef = processedState.object;
final scriptRef = processedState.script;
final next = objectHistory.peekNext();
if (next != null && next.obj.id == objRef.id) {
objectHistory.moveForward();
return;
}

final previous = objectHistory.peekPrevious();
if (previous != null && previous.obj.id == objRef.id) {
objectHistory.moveBack();
return;
}

final object = await createVmObject(objRef, scriptRef: scriptRef);
if (object != null) {
objectHistory.pushEntry(object);
}
}

Future<void> _onCurrentObjectChanged() async {
final currentObjectValue = objectHistory.current.value;

Expand Down Expand Up @@ -116,13 +147,25 @@ class ObjectInspectorViewController extends DisposableController
_refreshing.value = false;
}

Future<void> pushObject(ObjRef objRef, {ScriptRef? scriptRef}) async {
Future<void> pushObject(
BuildContext context,
ObjRef objRef, {
ScriptRef? scriptRef,
}) async {
_refreshing.value = true;

final object = await createVmObject(objRef, scriptRef: scriptRef);
if (object != null) {
objectHistory.pushEntry(object);
}
Router.navigate(context, () async {
final object = await createVmObject(objRef, scriptRef: scriptRef);
if (object != null) {
objectHistory.pushEntry(object);
routerDelegate?.updateStateIfChanged(
ObjectInspectorNavigationState(
object: objRef,
script: scriptRef,
),
);
}
});

_refreshing.value = false;
}
Expand Down Expand Up @@ -174,7 +217,7 @@ class ObjectInspectorViewController extends DisposableController

/// Re-initializes the object inspector's state when building it for the
/// first time or when the selected isolate is updated.
void _initializeForCurrentIsolate() async {
void initializeForCurrentIsolate(BuildContext context) async {
objectHistory.clear();
await objectStoreController.refresh();
await classHierarchyController.refresh();
Expand All @@ -198,15 +241,18 @@ class ObjectInspectorViewController extends DisposableController
if (parts.isEmpty) {
for (final lib in libraries) {
if (lib.uri == mainScriptRef.uri) {
return await pushObject(lib, scriptRef: mainScriptRef);
return await pushObject(context, lib, scriptRef: mainScriptRef);
}
}
}
await pushObject(mainScriptRef, scriptRef: mainScriptRef);
await pushObject(context, mainScriptRef, scriptRef: mainScriptRef);
}
}

Future<void> findAndSelectNodeForObject(ObjRef obj) async {
Future<void> findAndSelectNodeForObject(
BuildContext context,
ObjRef obj,
) async {
codeViewController.clearState();
ScriptRef? script;
try {
Expand All @@ -217,6 +263,55 @@ class ObjectInspectorViewController extends DisposableController
// the object store.
programExplorerController.clearSelection();
}
await pushObject(obj, scriptRef: script);
await pushObject(context, obj, scriptRef: script);
}
}

/// State used to inform [CodeViewController]s listening for
/// [DevToolsNavigationState] changes to display a specific source location.
class ObjectInspectorNavigationState extends DevToolsNavigationState {
ObjectInspectorNavigationState({
required ObjRef object,
ScriptRef? script,
}) : super(
kind: type,
state: <String, String?>{
_kObject: json.encode(object.json),
_kScript: json.encode(script?.json),
},
);

ObjectInspectorNavigationState._fromState(
DevToolsNavigationState state,
) : super(
kind: type,
state: state.state,
);

static ObjectInspectorNavigationState? fromState(
DevToolsNavigationState? state,
) {
if (state?.kind != type) return null;
return ObjectInspectorNavigationState._fromState(state!);
}

static const _kObject = 'object';
static const _kScript = 'script';
static const type = 'objectInspectorNavigationState';

ObjRef get object {
final obj = state[_kObject];
return createServiceObject(json.decode(obj!), const []) as ObjRef;
}

ScriptRef? get script {
final script = state[_kScript];
return createServiceObject(json.decode(script ?? '{}'), const [])
as ScriptRef;
}

@override
String toString() {
return 'kind: $kind object: ${object.id} script: ${script?.id}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:vm_service/vm_service.dart';
import '../../../shared/common_widgets.dart';
import '../../../shared/history_viewport.dart';
import '../../../shared/primitives/history_manager.dart';
import '../../../shared/routing.dart';
import 'object_inspector_view_controller.dart';
import 'vm_class_display.dart';
import 'vm_code_display.dart';
Expand Down Expand Up @@ -36,6 +37,14 @@ class ObjectViewport extends StatelessWidget {
ToolbarRefresh(onPressed: controller.refreshObject),
],
generateTitle: viewportTitle,
// We disable history for the viewport as the browser history will
// correctly update the contents of the viewport and having separate
// history navigation controls will make it difficult to keep state
// properly synchronized.
//
// We should revisit this if we eventually decide to ship on desktop or
// some platform that doesn't have built-in router navigation history.
historyEnabled: false,
contentBuilder: (context, _) {
return ValueListenableBuilder<bool>(
valueListenable: controller.refreshing,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ class DisplayProvider extends StatelessWidget {
},
onTap: (object) async {
if (object is ObjRef) {
await controller.findAndSelectNodeForObject(object);
await controller.findAndSelectNodeForObject(context, object);
}
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ MapEntry<String, WidgetBuilder>
textBuilder: textBuilder,
preferUri: preferUri,
onTap: (object) async {
await controller.findAndSelectNodeForObject(object);
await controller.findAndSelectNodeForObject(context, object);
},
),
);
Expand Down Expand Up @@ -424,7 +424,7 @@ class RetainingPathWidget extends StatelessWidget {
) {
final onTap = (ObjRef? obj) async {
if (obj == null) return;
await controller.findAndSelectNodeForObject(obj);
await controller.findAndSelectNodeForObject(context, obj);
};
final theme = Theme.of(context);
final emptyList = SelectableText(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@

import 'package:flutter/foundation.dart';

import '../../shared/routing.dart';
import 'object_inspector/object_inspector_view_controller.dart';
import 'vm_developer_tools_screen.dart';

class VMDeveloperToolsController {
VMDeveloperToolsController({
DevToolsRouterDelegate? routerDelegate,
ObjectInspectorViewController? objectInspectorViewController,
}) : objectInspectorViewController =
objectInspectorViewController ?? ObjectInspectorViewController();
objectInspectorViewController ?? ObjectInspectorViewController() {
if (routerDelegate != null) {
this
.objectInspectorViewController
.subscribeToRouterEvents(routerDelegate);
}
}

ValueListenable<int> get selectedIndex => _selectedIndex;
final _selectedIndex = ValueNotifier<int>(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ class HistoryManager<T> {
/// Returns null if there is no next value.
T? peekNext() => hasNext ? _history[_historyIndex + 1] : null;

/// Return the previous value.
///
/// Returns null if there is no previous value.
T? peekPrevious() => hasPrevious ? _history[_historyIndex - 1] : null;

/// Remove the most recent historical item on the stack.
///
/// If [current] is the last item on the stack when this method is called,
Expand Down