Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 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,134 @@ class OpenFileAction extends Action<OpenFileIntent> {
}
}

// ----- Stepping / pause keyboard intents (issue #3867) -----------------------
//
// These mirror the gating used by the debugger control buttons in
// `controls.dart`: stepping requires a paused, non-system isolate with at
// least one stack frame and no in-flight resume; pause/resume is gated by
// `isSystemIsolate` and the current paused state.

bool _isSystemIsolate(DebuggerController controller) =>
controller.isSystemIsolate;

bool _canStep(DebuggerController controller) {
return serviceConnection.serviceManager.isMainIsolatePaused &&
!controller.resuming.value &&
controller.stackFramesWithLocation.value.isNotEmpty &&
!controller.isSystemIsolate;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[CONCERN] The logic for determining if stepping is allowed is duplicated here and in controls.dart. To improve maintainability and ensure consistent behavior between buttons and shortcuts, this logic should be moved to a shared property or method in DebuggerController.

References
  1. Code Reuse: Use shared primitives and components rather than recreating them from scratch. (link)


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

final DebuggerController _controller;
}

class PauseResumeAction extends Action<PauseResumeIntent> {
@override
bool isEnabled(PauseResumeIntent intent) {
if (_isSystemIsolate(intent._controller)) {
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;
if (frames.length < 2) {
return;
}
final currentlySelected = controller.selectedStackFrame.value;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[NIT] This check is redundant as isEnabled already ensures that frames.length > 1. Removing it simplifies the method.

    final currentlySelected = controller.selectedStackFrame.value;
References
  1. Adhere to DRY (Don't Repeat Yourself) principles by removing redundant logic checks. (link)

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) => _canStep(intent._controller);

@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) => _canStep(intent._controller);

@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) => _canStep(intent._controller);

@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