Skip to content
Merged
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
17 changes: 17 additions & 0 deletions lib/core/actions/sql_editor_command_bridge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class SqlEditorCommandBridge {
VoidCallback? _onOpen;
VoidCallback? _onSave;
VoidCallback? _onExecute;
VoidCallback? _onExecuteStatement;
VoidCallback? _onCloseTab;
VoidCallback? _onNextTab;
VoidCallback? _onPrevTab;
Expand All @@ -36,6 +37,7 @@ class SqlEditorCommandBridge {
required VoidCallback onOpen,
required VoidCallback onSave,
VoidCallback? onExecute,
VoidCallback? onExecuteStatement,
VoidCallback? onCloseTab,
VoidCallback? onNextTab,
VoidCallback? onPrevTab,
Expand All @@ -48,6 +50,7 @@ class SqlEditorCommandBridge {
_onOpen = onOpen;
_onSave = onSave;
_onExecute = onExecute;
_onExecuteStatement = onExecuteStatement;
_onCloseTab = onCloseTab;
_onNextTab = onNextTab;
_onPrevTab = onPrevTab;
Expand All @@ -64,6 +67,7 @@ class SqlEditorCommandBridge {
_onOpen = null;
_onSave = null;
_onExecute = null;
_onExecuteStatement = null;
_onCloseTab = null;
_onNextTab = null;
_onPrevTab = null;
Expand Down Expand Up @@ -108,6 +112,18 @@ class SqlEditorCommandBridge {
pendingAction = SqlEditorPendingAction.executeQuery;
}

/// Runs the selection or the statement at the cursor (Ctrl/Cmd+Enter);
/// [invokeExecute] runs the whole script. Falls back to [invokeExecute] when
/// the workspace has no statement mode.
void invokeExecuteStatement() {
final statement = _onExecuteStatement;
if (statement != null) {
statement();
return;
}
invokeExecute();
}

void invokeCloseTab() {
_onCloseTab?.call();
}
Expand Down Expand Up @@ -176,6 +192,7 @@ class SqlEditorCommandBridge {
_onOpen = null;
_onSave = null;
_onExecute = null;
_onExecuteStatement = null;
_onCloseTab = null;
_onNextTab = null;
_onPrevTab = null;
Expand Down
29 changes: 23 additions & 6 deletions lib/features/main_screen/main_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ class _MainScreenState extends State<MainScreen> {
final physical = event.physicalKey;
final logical = event.logicalKey;

// 1. Execute SQL: F5, or (Ctrl/Cmd + Enter), or (Ctrl/Cmd + R)
// 1. Execute SQL, as in the editor: Ctrl/Cmd+Enter and Ctrl/Cmd+R run the
// selection or the statement at the cursor; F5 and Ctrl/Cmd+Shift+Enter
// run the whole script.
final isEnter = physical == PhysicalKeyboardKey.enter ||
physical == PhysicalKeyboardKey.numpadEnter ||
logical == LogicalKeyboardKey.enter ||
Expand All @@ -167,13 +169,28 @@ class _MainScreenState extends State<MainScreen> {
logical == LogicalKeyboardKey.keyR ||
logical == const LogicalKeyboardKey(0x0000043a); // Russian 'к'

if ((isCmdOrCtrl && isEnter) ||
isF5 ||
(isCmdOrCtrl && !isShift && !isAlt && isKeyR)) {
if (SqlEditorCommandBridge.instance.canExecute) {
final runScript = isF5 || (isCmdOrCtrl && isShift && !isAlt && isEnter);
final runStatement = isCmdOrCtrl &&
!isShift &&
!isAlt &&
(isEnter || isKeyR);
if ((runScript || runStatement) &&
SqlEditorCommandBridge.instance.canExecute) {
if (runScript) {
SqlEditorCommandBridge.instance.invokeExecute();
return true;
} else {
SqlEditorCommandBridge.instance.invokeExecuteStatement();
}
return true;
}

// Preferences: Ctrl+, / Cmd+, (Physical comma, Logical comma, Russian 'б')
final isComma = physical == PhysicalKeyboardKey.comma ||
logical == LogicalKeyboardKey.comma ||
logical == const LogicalKeyboardKey(0x00000431); // Russian 'б'
if (isCmdOrCtrl && !isShift && !isAlt && isComma) {
showPreferencesDialog(context);
return true;
}

// 2. Toggle Left Sidebar: Ctrl+B / Cmd+B (Physical B, Logical B, Russian 'и')
Expand Down
3 changes: 2 additions & 1 deletion lib/features/settings/preferences_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ class PreferencesDialogContentState
final matching = PreferencesShortcutsSection.allShortcuts.where(
(s) =>
s.action.toLowerCase().contains(q) ||
s.keys.any((k) => k.toLowerCase().contains(q)),
s.allKeys.any(
(combo) => combo.any((k) => k.toLowerCase().contains(q))),
);
count += matching.length;
case PreferencesCategory.about:
Expand Down
123 changes: 83 additions & 40 deletions lib/features/settings/preferences_shortcuts_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@ class ShortcutItem {
required this.category,
required this.action,
required this.keys,
this.alternativeKeys = const [],
this.description,
});

final String category;
final String action;
final List<String> keys;

/// Other key combinations bound to the same action, shown after "or".
final List<List<String>> alternativeKeys;
final String? description;

/// [keys] and then every [alternativeKeys] combination.
List<List<String>> get allKeys => [keys, ...alternativeKeys];
}

/// Interactive shortcuts reference table grouped by category with OS-aware keycaps.
Expand All @@ -32,12 +39,18 @@ class PreferencesShortcutsSection extends material.StatefulWidget {
category: 'SQL Editor',
action: 'Execute query / selection',
keys: [_modKey, 'Enter'],
alternativeKeys: [
[_modKey, 'R'],
],
description: 'Runs statement under cursor or selected text',
),
ShortcutItem(
category: 'SQL Editor',
action: 'Execute all statements',
keys: [_modKey, 'Shift', 'Enter'],
alternativeKeys: const [
['F5'],
],
description: 'Runs the script one statement at a time, stops at the first error',
),
ShortcutItem(
Expand Down Expand Up @@ -126,6 +139,18 @@ class PreferencesShortcutsSection extends material.StatefulWidget {
keys: ['Right Click', 'Filter'],
description: 'Filters grid by value matching selected cell',
),
const ShortcutItem(
category: 'Data Grid',
action: 'Refresh table data',
keys: ['F5'],
description: 'Reloads the rows in the table browser',
),
ShortcutItem(
category: 'Data Grid',
action: 'Toggle edit mode',
keys: [_modKey, 'E'],
description: 'Turns cell editing on or off in the table browser',
),

// Navigation & App
ShortcutItem(
Expand All @@ -146,11 +171,17 @@ class PreferencesShortcutsSection extends material.StatefulWidget {
keys: [_modKey, ','],
description: 'Opens application settings dialog',
),
const ShortcutItem(
ShortcutItem(
category: 'Navigation & App',
action: 'Refresh schema / connection',
keys: ['F5'],
description: 'Refreshes databases and table tree',
action: 'Command palette',
keys: [_modKey, 'P'],
description: 'Searches and runs any command',
),
ShortcutItem(
category: 'Navigation & App',
action: 'Quick switcher',
keys: [_modKey, 'K'],
description: 'Jumps to a connection, database or table',
),
ShortcutItem(
category: 'Navigation & App',
Expand Down Expand Up @@ -191,7 +222,8 @@ class _PreferencesShortcutsSectionState
final matchAction = item.action.toLowerCase().contains(q);
final matchCategory = item.category.toLowerCase().contains(q);
final matchDesc = item.description?.toLowerCase().contains(q) ?? false;
final matchKeys = item.keys.any((k) => k.toLowerCase().contains(q));
final matchKeys = item.allKeys
.any((combo) => combo.any((k) => k.toLowerCase().contains(q)));
return matchAction || matchCategory || matchDesc || matchKeys;
}).toList();

Expand Down Expand Up @@ -352,48 +384,59 @@ class _PreferencesShortcutsSectionState
material.Row(
mainAxisSize: material.MainAxisSize.min,
children: [
for (var j = 0; j < item.keys.length; j++) ...[
if (j > 0)
for (var c = 0; c < item.allKeys.length; c++) ...[
if (c > 0)
material.Padding(
padding: const material.EdgeInsets.symmetric(horizontal: 3),
child: const Text('+')
.muted()
.xSmall(),
),
material.Container(
padding: const material.EdgeInsets.symmetric(
horizontal: 7,
vertical: 3,
padding: const material.EdgeInsets.symmetric(horizontal: 6),
child: const Text('or').muted().xSmall(),
),
decoration: material.BoxDecoration(
color: theme.colorScheme.background,
borderRadius: material.BorderRadius.circular(4),
border: material.Border.all(
color: theme.colorScheme.border.withValues(alpha: 0.4),
),
boxShadow: [
material.BoxShadow(
color: material.Colors.black.withValues(alpha: 0.08),
offset: const material.Offset(0, 1),
blurRadius: 1,
),
],
),
child: material.Text(
item.keys[j],
style: material.TextStyle(
fontFamily: QueryaTypography.mono,
fontSize: 11,
fontWeight: material.FontWeight.w600,
color: theme.colorScheme.foreground,
),
),
),
..._keycaps(item.allKeys[c], theme),
],
],
),
],
),
);
}

List<material.Widget> _keycaps(List<String> keys, ThemeData theme) {
return [
for (var j = 0; j < keys.length; j++) ...[
if (j > 0)
material.Padding(
padding: const material.EdgeInsets.symmetric(horizontal: 3),
child: const Text('+').muted().xSmall(),
),
material.Container(
padding: const material.EdgeInsets.symmetric(
horizontal: 7,
vertical: 3,
),
decoration: material.BoxDecoration(
color: theme.colorScheme.background,
borderRadius: material.BorderRadius.circular(4),
border: material.Border.all(
color: theme.colorScheme.border.withValues(alpha: 0.4),
),
boxShadow: [
material.BoxShadow(
color: material.Colors.black.withValues(alpha: 0.08),
offset: const material.Offset(0, 1),
blurRadius: 1,
),
],
),
child: material.Text(
keys[j],
style: material.TextStyle(
fontFamily: QueryaTypography.mono,
fontSize: 11,
fontWeight: material.FontWeight.w600,
color: theme.colorScheme.foreground,
),
),
),
],
];
}
}
20 changes: 15 additions & 5 deletions lib/features/workspace/generic_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
super.dispose();
}

void _formatActive() {
if (_activeSession.running) return;
_activeSession.formatSql();
invalidatePane(_activeSession);
setState(() {});
}

void _registerSqlEditorCommands() {
if (!mounted) return;
SqlEditorCommandBridge.instance.register(
Expand All @@ -229,16 +236,17 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
onExecute: () {
if (!_activeSession.running) unawaited(execute(_activeSession));
},
onExecuteStatement: () {
if (!_activeSession.running) {
unawaited(execute(_activeSession, true));
}
},
onCloseTab: () {
if (_sessions.length > 1) unawaited(closeTab(_activeSessionIndex));
},
onNextTab: nextTab,
onPrevTab: prevTab,
onFormat: () {
_activeSession.formatSql();
invalidatePane(_activeSession);
setState(() {});
},
onFormat: _formatActive,
onClear: () {
_activeSession.clearSql();
invalidatePane(_activeSession);
Expand Down Expand Up @@ -1201,6 +1209,8 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
const material.SingleActivator(LogicalKeyboardKey.keyR, meta: true): () {
if (!_activeSession.running) unawaited(execute(_activeSession, true));
},
const material.SingleActivator(LogicalKeyboardKey.keyF, shift: true, alt: true):
_formatActive,
},
child: material.Focus(
autofocus: true,
Expand Down
33 changes: 33 additions & 0 deletions test/core/actions/sql_editor_command_bridge_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,37 @@ void main() {
bridge.unregister(connectionId: 3);
expect(bridge.isActive, isFalse);
});

// #1148: outside the editor, Ctrl/Cmd+Enter runs the statement at the
// cursor and F5 runs the script, as inside it.
test('invokeExecuteStatement runs the statement, invokeExecute the script',
() {
final ran = <String>[];
SqlEditorCommandBridge.instance.register(
connectionId: 7,
onNew: () {},
onOpen: () {},
onSave: () {},
onExecute: () => ran.add('script'),
onExecuteStatement: () => ran.add('statement'),
);

SqlEditorCommandBridge.instance.invokeExecuteStatement();
SqlEditorCommandBridge.instance.invokeExecute();
expect(ran, ['statement', 'script']);
});

test('without a statement mode, invokeExecuteStatement runs the script', () {
final ran = <String>[];
SqlEditorCommandBridge.instance.register(
connectionId: 7,
onNew: () {},
onOpen: () {},
onSave: () {},
onExecute: () => ran.add('script'),
);

SqlEditorCommandBridge.instance.invokeExecuteStatement();
expect(ran, ['script']);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ void main() {
await tester.pumpWidget(buildTestWidget(searchQuery: 'F5'));
await tester.pumpAndSettle();

expect(find.text('Refresh schema / connection'), findsOneWidget);
// F5 runs the script in the SQL editor and reloads the table browser;
// it does not refresh the connection tree (#1148).
expect(find.text('Execute all statements'), findsOneWidget);
expect(find.text('Refresh table data'), findsOneWidget);
expect(find.text('Refresh schema / connection'), findsNothing);
expect(find.text('Execute query / selection'), findsNothing);
});

Expand Down
Loading
Loading