[CB] SQL editor - ctrl+z during autosave scrolls the script to the top #5645 (#3470)

* dbeaver/pro#5645 removes scroll to top for sql editor

* removes floating second scroll bar on ctrl+z

* removes cursor jumping + removes scrolling to top

* reverts layout scroll behavior

* reverts layout scroll behavior 2

* removes unneeded cursorOffset

* removes offset and unused code

* reverts has insert property behavior

* remembers previous cursor position from history

* adds mobx action

* reverts insert property logic

* fixes cursor position during ctrl+z

* Revert "fixes cursor position during ctrl+z"

This reverts commit d1aac8e750.

* dbeaver/pro#5645 sets cursor in sync with code mirror onChange event

* eslint fix

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
sergeyteleshev
2025-06-23 19:44:18 +08:00
committed by GitHub
co-authored by Evgenia
parent ab0df706bb
commit beecc84d14
16 changed files with 91 additions and 60 deletions
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -24,7 +24,7 @@ export interface IReactCodeMirrorProps extends React.PropsWithChildren {
readonly?: boolean;
copyEventHandler?: (event: ClipboardEvent) => boolean;
autoFocus?: boolean;
onChange?: (value: string, update: ViewUpdate) => void;
onChange?: (value: string, selection: SelectionRange, update: ViewUpdate) => void;
onCursorChange?: (selection: SelectionRange, update: ViewUpdate) => void;
onUpdate?: (update: ViewUpdate) => void;
}
@@ -13,13 +13,13 @@ import { forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useRef, useS
import { useObjectRef } from '@cloudbeaver/core-blocks';
import { hasInsertProperty } from './hasInsertProperty.js';
import type { IEditorRef } from './IEditorRef.js';
import type { IReactCodeMirrorProps } from './IReactCodemirrorProps.js';
import { type IReactCodemirrorContext, ReactCodemirrorContext } from './ReactCodemirrorContext.js';
import { useCodemirrorExtensions } from './useCodemirrorExtensions.js';
import { validateCursorBoundaries } from './validateCursorBoundaries.js';
import { ReactCodemirrorSearchPanel } from './ReactCodemirrorSearchPanel.js';
import { hasInsertProperty } from './hasInsertProperty.js';
const External = Annotation.define<boolean>();
@@ -67,16 +67,16 @@ export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(
if (container) {
const updateListener = EditorView.updateListener.of((update: ViewUpdate) => {
const remote = update.transactions.some(tr => tr.annotation(External));
const selection = update.state.selection.main;
if (update.docChanged && !remote) {
const doc = update.state.doc;
const value = doc.toString();
callbackRef.onChange?.(value, update);
callbackRef.onChange?.(value, selection, update);
}
if (update.selectionSet && !remote) {
const selection = update.state.selection.main;
callbackRef.onCursorChange?.(selection, update);
}
@@ -221,8 +221,12 @@ export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(
};
}
if (transaction.changes || transaction.selection) {
view.dispatch(transaction);
if (transaction.changes) {
view.dispatch({ changes: transaction.changes });
}
if (transaction.selection) {
view.dispatch({ selection: transaction.selection });
}
}
});
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -9,4 +9,4 @@ import type { ChangeSpec, Text } from '@codemirror/state';
export function hasInsertProperty(spec: ChangeSpec | undefined): spec is { from: number; to?: number; insert?: string | Text } {
return typeof spec === 'object' && spec !== null && 'insert' in spec;
}
}
@@ -21,7 +21,7 @@ import type { NetworkStateService } from '@cloudbeaver/core-root';
import { debounce, getPathName, isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
import { isNotNullDefined } from '@dbeaver/js-helpers';
import { SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts';
import { BaseSqlDataSource, ESqlDataSourceFeatures, SqlEditorService } from '@cloudbeaver/plugin-sql-editor';
import { BaseSqlDataSource, ESqlDataSourceFeatures, SqlEditorService, type ISqlEditorCursor } from '@cloudbeaver/plugin-sql-editor';
import type { IResourceSqlDataSourceState } from './IResourceSqlDataSourceState.js';
@@ -219,14 +219,14 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
super.setProject(projectId);
}
override setScript(script: string): void {
override setScript(script: string, source?: string, cursor?: ISqlEditorCursor): void {
const previous = this.state.script;
if (previous === script) {
return;
}
this.state.script = script;
super.setScript(script);
super.setScript(script, source, cursor);
if (this.isAutoSaveEnabled) {
this.debouncedWrite();
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -10,16 +10,18 @@ import { useCallback } from 'react';
import { useExecutor, useObservableRef } from '@cloudbeaver/core-blocks';
import { throttle } from '@cloudbeaver/core-utils';
import type { ISQLEditorData } from '@cloudbeaver/plugin-sql-editor';
import type { ISqlEditorCursor, ISQLEditorData } from '@cloudbeaver/plugin-sql-editor';
import type { IEditor } from '../SQLCodeEditor/useSQLCodeEditor.js';
interface State {
highlightActiveQuery: () => void;
onQueryChange: (query: string) => void;
onQueryChange: (query: string, selection: ISqlEditorCursor) => void;
onCursorChange: (anchor: number, head?: number) => void;
}
export const ON_QUERY_CHANGE_SOURCE = 'QueryChange';
export function useSQLCodeEditorPanel(data: ISQLEditorData, editor: IEditor) {
const state: State = useObservableRef(
() => ({
@@ -32,8 +34,9 @@ export function useSQLCodeEditorPanel(data: ISQLEditorData, editor: IEditor) {
this.editor.highlightActiveQuery(segment.begin, segment.end);
}
},
onQueryChange(query: string) {
this.data.setScript(query);
onQueryChange(query: string, selection: ISqlEditorCursor) {
this.data.setScript(query, ON_QUERY_CHANGE_SOURCE, selection);
this.onCursorChange(selection.anchor, selection.head);
},
onCursorChange(anchor: number, head?: number) {
this.data.setCursor(anchor, head);
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -104,14 +104,24 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
this.onDatabaseModelUpdate.setInitialDataGetter(() => this.databaseModels);
this.onSetScript.next(this.onUpdate);
this.onSetScript.addHandler(({ script, source }) => {
if (source === SOURCE_HISTORY) {
return;
}
this.history.add(script);
});
this.onSetScript.addHandler(
action(({ script, source, cursor }) => {
if (source === SOURCE_HISTORY) {
return;
}
this.history.add(script, source, cursor);
}),
);
this.history.onNavigate.addHandler(value => this.setScript(value, SOURCE_HISTORY));
this.history.onNavigate.addHandler(
action(({ value, cursor }) => {
this.setScript(value, SOURCE_HISTORY);
if (cursor) {
this.setCursor(cursor.anchor, cursor.head);
}
}),
);
makeObservable<this, 'outdated' | 'editing' | 'innerCursorState'>(this, {
isSaved: computed,
@@ -142,8 +152,8 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
});
}
setScript(script: string, source?: string): void {
this.onSetScript.execute({ script, source });
setScript(script: string, source?: string, cursor?: ISqlEditorCursor): void {
this.onSetScript.execute({ script, source, cursor });
}
setIncomingScript(script: string): void {
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ export interface ISqlDataSourceKey {
export interface ISetScriptData {
script: string;
cursor?: ISqlEditorCursor;
source?: string;
}
@@ -70,7 +71,7 @@ export interface ISqlDataSource extends ILoadableState {
setName(name: string | null): void;
setProject(projectId: string | null): void;
setScript(script: string, source?: string): void;
setScript(script: string, source?: string, cursor?: ISqlEditorCursor): void;
setCursor(anchor: number, head?: number): void;
setEditing(state: boolean): void;
setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void;
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -12,6 +12,7 @@ import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connecti
import { BaseSqlDataSource } from '../BaseSqlDataSource.js';
import { ESqlDataSourceFeatures } from '../ESqlDataSourceFeatures.js';
import type { ILocalStorageSqlDataSourceState } from './ILocalStorageSqlDataSourceState.js';
import type { ISqlEditorCursor } from '../ISqlDataSource.js';
export class LocalStorageSqlDataSource extends BaseSqlDataSource {
get baseScript(): string {
@@ -75,9 +76,9 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource {
return true;
}
override setScript(script: string): void {
override setScript(script: string, source?: string, cursor?: ISqlEditorCursor): void {
this.state.script = script;
super.setScript(script);
super.setScript(script, source, cursor);
}
override setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void {
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -11,6 +11,7 @@ import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connecti
import { BaseSqlDataSource } from './BaseSqlDataSource.js';
import { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures.js';
import type { ISqlEditorCursor } from './ISqlDataSource.js';
export class MemorySqlDataSource extends BaseSqlDataSource {
get baseScript(): string {
@@ -69,9 +70,9 @@ export class MemorySqlDataSource extends BaseSqlDataSource {
return false;
}
override setScript(script: string): void {
override setScript(script: string, source?: string, cursor?: ISqlEditorCursor): void {
this._script = script;
super.setScript(script);
super.setScript(script, source, cursor);
}
override setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void {
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -8,11 +8,15 @@
import type { ISyncExecutor } from '@cloudbeaver/core-executor';
import type { ISqlDataSourceHistoryState } from './ISqlDataSourceHistoryState.js';
import type { ISqlEditorCursor } from '../ISqlDataSource.js';
export interface ISqlDataSourceHistory {
readonly state: ISqlDataSourceHistoryState;
readonly onNavigate: ISyncExecutor<string>;
add(value: string, source?: string): void;
readonly onNavigate: ISyncExecutor<{
value: string;
cursor?: ISqlEditorCursor;
}>;
add(value: string, source?: string, cursor?: ISqlEditorCursor): void;
undo(): void;
redo(): void;
restore(data: ISqlDataSourceHistoryState): void;
@@ -1,13 +1,16 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { ISqlEditorCursor } from '../ISqlDataSource.js';
export interface ISqlDataSourceHistoryData {
value: string;
cursor?: ISqlEditorCursor;
timestamp: number;
source?: string;
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -12,13 +12,17 @@ import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { createSqlDataSourceHistoryInitialState } from './createSqlDataSourceHistoryInitialState.js';
import type { ISqlDataSourceHistory } from './ISqlDataSourceHistory.js';
import type { ISqlDataSourceHistoryState } from './ISqlDataSourceHistoryState.js';
import type { ISqlEditorCursor } from '../ISqlDataSource.js';
const HOT_HISTORY_SIZE = 30;
const COMPRESSED_HISTORY_DELAY = 5000;
export class SqlDataSourceHistory implements ISqlDataSourceHistory {
state: ISqlDataSourceHistoryState;
readonly onNavigate: ISyncExecutor<string>;
readonly onNavigate: ISyncExecutor<{
value: string;
cursor?: ISqlEditorCursor;
}>;
constructor() {
this.state = createSqlDataSourceHistoryInitialState();
@@ -29,7 +33,7 @@ export class SqlDataSourceHistory implements ISqlDataSourceHistory {
});
}
add(value: string, source?: string): void {
add(value: string, source?: string, cursor?: ISqlEditorCursor): void {
// skip history if value is the same as current
if (this.state.history[this.state.historyIndex]!.value === value) {
return;
@@ -40,7 +44,7 @@ export class SqlDataSourceHistory implements ISqlDataSourceHistory {
this.state.history.splice(this.state.historyIndex + 1);
}
this.state.historyIndex = this.state.history.push({ value, source, timestamp: Date.now() }) - 1;
this.state.historyIndex = this.state.history.push({ value, source, timestamp: Date.now(), cursor }) - 1;
this.compressHistory();
}
@@ -49,8 +53,8 @@ export class SqlDataSourceHistory implements ISqlDataSourceHistory {
return;
}
this.state.historyIndex--;
const value = this.state.history[this.state.historyIndex]!.value;
this.onNavigate.execute(value);
const prevHistoryItem = this.state.history[this.state.historyIndex]!;
this.onNavigate.execute(prevHistoryItem);
}
redo(): void {
@@ -59,8 +63,8 @@ export class SqlDataSourceHistory implements ISqlDataSourceHistory {
}
this.state.historyIndex++;
const value = this.state.history[this.state.historyIndex]!.value;
this.onNavigate.execute(value);
const prevHistoryItem = this.state.history[this.state.historyIndex]!;
this.onNavigate.execute(prevHistoryItem);
}
restore(state: ISqlDataSourceHistoryState): void {
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -9,7 +9,7 @@ import type { ISqlDataSourceHistoryState } from './ISqlDataSourceHistoryState.js
export function createSqlDataSourceHistoryInitialState(value = ''): ISqlDataSourceHistoryState {
return {
history: [{ value, source: 'initial', timestamp: Date.now() }],
history: [{ value, source: 'initial', timestamp: Date.now(), cursor: { anchor: 0, head: 0 } }],
historyIndex: 0,
};
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -42,7 +42,7 @@ export interface ISQLEditorData {
readonly hintsLimitIsMet: boolean;
updateParserScriptsDebounced(): Promise<void>;
setScript(query: string): void;
setScript(query: string, source?: string, cursor?: ISqlEditorCursor): void;
init(): void;
destruct(): void;
setCursor(begin: number, end?: number): void;
@@ -51,12 +51,12 @@ export interface ISQLEditorData {
executeQueryNewTab(): Promise<void>;
showExecutionPlan(): Promise<void>;
executeScript(): Promise<void>;
switchEditing(): Promise<void>;
switchEditing(): void;
getHintProposals(position: number, simple: boolean): Promise<SQLProposal[]>;
getResolvedSegment(): Promise<ISQLScriptSegment | undefined>;
executeQueryAction<T>(
segment: ISQLScriptSegment | undefined,
action: (query: ISQLScriptSegment) => Promise<T>,
action: (query: ISQLScriptSegment) => T | Promise<T>,
passEmpty?: boolean,
passDisabled?: boolean,
): Promise<T | undefined>;
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -251,7 +251,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
await this.executeQueryAction(
query,
async () => {
() => {
if (this.dataSource?.databaseModels.length) {
this.sqlQueryService.initDatabaseDataModels(this.state);
}
@@ -297,7 +297,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
} catch {}
},
async switchEditing(): Promise<void> {
switchEditing(): void {
this.dataSource?.setEditing(!this.dataSource.isEditing());
},
@@ -360,8 +360,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
}
},
setScript(query: string): void {
this.dataSource?.setScript(query);
setScript(query: string, source?: string, cursor?: ISqlEditorCursor): void {
this.dataSource?.setScript(query, source, cursor);
},
updateParserScriptsDebounced: debounceAsync(async function updateParserScriptsThrottle() {
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -174,7 +174,7 @@ export class SqlEditorService {
}
}
async initEditorConnection(state: ISqlEditorTabState): Promise<IConnectionExecutionContext | undefined> {
initEditorConnection(state: ISqlEditorTabState): Promise<IConnectionExecutionContext | undefined> {
return this.sqlDataSourceService.executeAction(
state.editorId,
async dataSource => {