Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,7 @@ class _DebuggingControlsState extends State<DebuggingControls>
@override
Widget build(BuildContext context) {
final resuming = controller.resuming.value;
final hasStackFrames = controller.stackFramesWithLocation.value.isNotEmpty;
final isSystemIsolate = controller.isSystemIsolate;
final canStep =
serviceConnection.serviceManager.isMainIsolatePaused &&
!resuming &&
hasStackFrames &&
!isSystemIsolate;
final canStep = controller.canStep;
final isVmApp =
serviceConnection.serviceManager.connectedApp?.isRunningOnDartVM ??
false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,18 @@ class DebuggerController extends DevToolsScreenController

bool get isSystemIsolate => _isolate.value?.isSystemIsolate ?? false;

/// Whether the debugger is currently in a state where step operations
/// (Step Over / In / Out) can be performed.
///
/// Used by the debugger control buttons in `controls.dart` and by the
/// keyboard-shortcut Actions in `debugger_screen.dart` so that both
/// surfaces gate on the same conditions.
bool get canStep =>
serviceConnection.serviceManager.isMainIsolatePaused &&
!resuming.value &&
stackFramesWithLocation.value.isNotEmpty &&
!isSystemIsolate;

String get _isolateRefId {
final id = _isolate.value?.id;
if (id == null) return '';
Expand Down
124 changes: 124 additions & 0 deletions packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,22 @@ class DebuggerScreen extends Screen {
searchInFileKeySet: SearchInFileIntent(controller),
escapeKeySet: EscapeIntent(controller),
openFileKeySet: OpenFileIntent(controller),
pauseResumeKeySet: PauseResumeIntent(controller),
nextStackFrameKeySet: NextStackFrameIntent(controller),
stepOverKeySet: StepOverIntent(controller),
stepInKeySet: StepInIntent(controller),
stepOutKeySet: StepOutIntent(controller),
};
final actions = <Type, Action<Intent>>{
GoToLineNumberIntent: GoToLineNumberAction(),
SearchInFileIntent: SearchInFileAction(),
EscapeIntent: EscapeAction(),
OpenFileIntent: OpenFileAction(),
PauseResumeIntent: PauseResumeAction(),
NextStackFrameIntent: NextStackFrameAction(),
StepOverIntent: StepOverAction(),
StepInIntent: StepInAction(),
StepOutIntent: StepOutAction(),
};
return ShortcutsConfiguration(shortcuts: shortcuts, actions: actions);
}
Expand Down Expand Up @@ -412,6 +422,120 @@ class OpenFileAction extends Action<OpenFileIntent> {
}
}

// ----- Stepping / pause keyboard intents (issue #3867) -----------------------
//
// Each Action delegates to `DebuggerController` for both the gating
// (`isEnabled`) and the side-effecting call (`invoke`), so the keyboard
// shortcuts stay in lock-step with the debugger buttons in `controls.dart`.

class PauseResumeIntent extends Intent {
const PauseResumeIntent(this._controller);

final DebuggerController _controller;
}

class PauseResumeAction extends Action<PauseResumeIntent> {
@override
bool isEnabled(PauseResumeIntent intent) {
if (intent._controller.isSystemIsolate) {
return false;
}
final isPaused = serviceConnection.serviceManager.isMainIsolatePaused;
if (isPaused) {
// When paused, the button becomes "Resume" and is disabled while a
// resume is already in flight.
return !intent._controller.resuming.value;
}
return true;
}

@override
void invoke(PauseResumeIntent intent) {
final controller = intent._controller;
if (serviceConnection.serviceManager.isMainIsolatePaused) {
unawaited(controller.resume());
} else {
unawaited(controller.pause());
}
}
}

class NextStackFrameIntent extends Intent {
const NextStackFrameIntent(this._controller);

final DebuggerController _controller;
}

class NextStackFrameAction extends Action<NextStackFrameIntent> {
@override
bool isEnabled(NextStackFrameIntent intent) =>
intent._controller.stackFramesWithLocation.value.length > 1;

@override
void invoke(NextStackFrameIntent intent) {
final controller = intent._controller;
final frames = controller.stackFramesWithLocation.value;
final currentlySelected = controller.selectedStackFrame.value;
final currentIndex = currentlySelected == null
? -1
: frames.indexWhere(
(frame) => frame.frame.index == currentlySelected.frame.index,
);
// Cycle through frames so repeated presses walk the call stack and wrap
// back to the top.
final nextIndex = (currentIndex + 1) % frames.length;
unawaited(controller.selectStackFrame(frames[nextIndex]));
}
}

class StepOverIntent extends Intent {
const StepOverIntent(this._controller);

final DebuggerController _controller;
}

class StepOverAction extends Action<StepOverIntent> {
@override
bool isEnabled(StepOverIntent intent) => intent._controller.canStep;

@override
void invoke(StepOverIntent intent) {
unawaited(intent._controller.stepOver());
}
}

class StepInIntent extends Intent {
const StepInIntent(this._controller);

final DebuggerController _controller;
}

class StepInAction extends Action<StepInIntent> {
@override
bool isEnabled(StepInIntent intent) => intent._controller.canStep;

@override
void invoke(StepInIntent intent) {
unawaited(intent._controller.stepIn());
}
}

class StepOutIntent extends Intent {
const StepOutIntent(this._controller);

final DebuggerController _controller;
}

class StepOutAction extends Action<StepOutIntent> {
@override
bool isEnabled(StepOutIntent intent) => intent._controller.canStep;

@override
void invoke(StepOutIntent intent) {
unawaited(intent._controller.stepOut());
}
}

class DebuggerStatus extends StatefulWidget {
const DebuggerStatus({super.key, required this.controller});

Expand Down
20 changes: 20 additions & 0 deletions packages/devtools_app/lib/src/screens/debugger/key_sets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,23 @@ final openFileKeySet = LogicalKeySet(
final openFileKeySetDescription = openFileKeySet.describeKeys(
isMacOS: HostPlatform.instance.isMacOS,
);

// Debugger stepping / pause shortcuts.
//
// Bindings mirror Chrome DevTools (F8 / F9 / F10 / F11 / Shift+F11), which
// VS Code also uses for the stepping triplet. Aligning with these surfaces
// keeps the debugger feel familiar across IDEs.
//
// See https://github.com/flutter/devtools/issues/3867.
final pauseResumeKeySet = LogicalKeySet(LogicalKeyboardKey.f8);

final nextStackFrameKeySet = LogicalKeySet(LogicalKeyboardKey.f9);

final stepOverKeySet = LogicalKeySet(LogicalKeyboardKey.f10);

final stepInKeySet = LogicalKeySet(LogicalKeyboardKey.f11);

final stepOutKeySet = LogicalKeySet(
LogicalKeyboardKey.shift,
LogicalKeyboardKey.f11,
);
Loading