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

CB-4740 fix(plugin-codemirror6): value equal check #2414

Merged
merged 7 commits into from
Feb 27, 2024
2 changes: 0 additions & 2 deletions webapp/packages/plugin-codemirror6/src/IEditorRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import type { EditorView } from '@codemirror/view';
import type { SelectionRange } from '@codemirror/state';

export interface IEditorRef {
container: HTMLDivElement | null;
view: EditorView | null;
selection: SelectionRange | null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { Compartment, Extension, SelectionRange } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view';

/** Currently we support only main selection range */
interface ISelection {
export interface ISelection {
anchor: number;
head?: number;
}
Expand Down
48 changes: 33 additions & 15 deletions webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { MergeView } from '@codemirror/merge';
import { Annotation, Compartment, Extension, StateEffect, TransactionSpec } from '@codemirror/state';
import { Annotation, Compartment, EditorState, Extension, StateEffect, TransactionSpec } from '@codemirror/state';
import { EditorView, ViewUpdate } from '@codemirror/view';
import { observer } from 'mobx-react-lite';
import { forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
Expand All @@ -17,6 +17,7 @@ import type { IEditorRef } from './IEditorRef';
import type { IReactCodeMirrorProps } from './IReactCodemirrorProps';
import { type IReactCodemirrorContext, ReactCodemirrorContext } from './ReactCodemirrorContext';
import { useCodemirrorExtensions } from './useCodemirrorExtensions';
import { validateCursorBoundaries } from './validateCursorBoundaries';

const External = Annotation.define<boolean>();

Expand Down Expand Up @@ -55,15 +56,11 @@ export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(
const [view, setView] = useState<EditorView | null>(null);
const [incomingView, setIncomingView] = useState<EditorView | null>(null);
const callbackRef = useObjectRef({ onChange, onCursorChange, onUpdate });
const [selection, setSelection] = useState(view?.state.selection.main ?? null);

useLayoutEffect(() => {
if (container) {
const updateListener = EditorView.updateListener.of((update: ViewUpdate) => {
const remote = update.transactions.some(tr => tr.annotation(External));
if (update.selectionSet) {
setSelection(update.state.selection.main);
}

if (update.docChanged && !remote) {
const doc = update.state.doc;
Expand All @@ -89,9 +86,15 @@ export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(
effects.push(compartment.of(extension));
}

const tempState = EditorState.create({
doc: value,
});

if (incomingValue !== undefined) {
merge = new MergeView({
a: {
doc: value,
selection: cursor && validateCursorBoundaries(cursor, tempState.doc.length),
extensions: [updateListener, ...effects],
},
b: {
Expand All @@ -104,11 +107,19 @@ export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(
incomingView = merge.b;
} else {
editorView = new EditorView({
state: EditorState.create({
doc: value,
selection: cursor && validateCursorBoundaries(cursor, tempState.doc.length),
extensions: [updateListener, ...effects],
}),
parent: container,
extensions: [updateListener, ...effects],
});
}

editorView.dispatch({
scrollIntoView: true,
});

if (incomingView) {
setIncomingView(incomingView);
}
Expand Down Expand Up @@ -168,9 +179,13 @@ export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(

let isCursorInDoc = cursor && cursor.anchor > 0 && cursor.anchor < view.state.doc.length;

if (value !== undefined && value !== view.state.doc.toString()) {
transaction.changes = { from: 0, to: view.state.doc.length, insert: value };
isCursorInDoc = cursor && cursor.anchor > 0 && cursor.anchor < value.length;
if (value !== undefined) {
const newText = view.state.toText(value);

if (!newText.eq(view.state.doc)) {
transaction.changes = { from: 0, to: view.state.doc.length, insert: newText };
isCursorInDoc = cursor && cursor.anchor > 0 && cursor.anchor < newText.length;
}
}

if (cursor && isCursorInDoc && (view.state.selection.main.anchor !== cursor.anchor || view.state.selection.main.head !== cursor.head)) {
Wroud marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -184,10 +199,14 @@ export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(
});

useLayoutEffect(() => {
if (incomingValue !== undefined && incomingView && incomingValue !== incomingView.state.doc.toString()) {
incomingView.dispatch({
changes: { from: 0, to: incomingView.state.doc.length, insert: incomingValue },
});
if (incomingValue !== undefined && incomingView) {
const newValue = incomingView.state.toText(incomingValue);

if (!newValue.eq(incomingView.state.doc)) {
incomingView.dispatch({
changes: { from: 0, to: incomingView.state.doc.length, insert: newValue },
});
}
}
}, [incomingValue, incomingView]);

Expand All @@ -202,9 +221,8 @@ export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(
() => ({
container,
view,
selection,
}),
[container, view, selection],
[container, view],
);

const context = useMemo<IReactCodemirrorContext>(
Expand Down
15 changes: 15 additions & 0 deletions webapp/packages/plugin-codemirror6/src/validateCursorBoundaries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { ISelection } from './IReactCodemirrorProps';

export function validateCursorBoundaries(selection: ISelection, documentLength: number): ISelection {
return {
anchor: Math.min(selection.anchor, documentLength),
head: selection.head === undefined ? undefined : Math.min(selection.head, documentLength),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { useEffect, useState } from 'react';
import { useState } from 'react';

import { MenuBarSmallItem, useExecutor, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { DATA_CONTEXT_NAV_NODE, getNodesFromContext, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { TabContainerPanelComponent, useDNDBox, useTabLocalState } from '@cloudbeaver/core-ui';
import { TabContainerPanelComponent, useDNDBox } from '@cloudbeaver/core-ui';
import { closeCompletion, IEditorRef, Prec, ReactCodemirrorPanel, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6';
import type { ISqlEditorModeProps } from '@cloudbeaver/plugin-sql-editor';

Expand All @@ -26,36 +26,16 @@ import style from './SQLCodeEditorPanel.m.css';
import { SqlEditorInfoBar } from './SqlEditorInfoBar';
import { useSQLCodeEditorPanel } from './useSQLCodeEditorPanel';

interface ILocalSQLCodeEditorPanelState {
selection: { from: number; to: number };
}

export const SQLCodeEditorPanel: TabContainerPanelComponent<ISqlEditorModeProps> = observer(function SQLCodeEditorPanel({ data }) {
const notificationService = useService(NotificationService);
const navNodeManagerService = useService(NavNodeManagerService);
const translate = useTranslate();
const localState = useTabLocalState<ILocalSQLCodeEditorPanelState>(() => ({ selection: { from: 0, to: 0 } }));

const styles = useS(style);
const [editorRef, setEditorRef] = useState<IEditorRef | null>(null);

const editor = useSQLCodeEditor(editorRef);

useEffect(() => {
editorRef?.view?.dispatch({
selection: { anchor: Math.min(localState.selection.to, data.value.length), head: Math.min(localState.selection.to, data.value.length) },
scrollIntoView: true,
});
}, [editorRef?.view, localState, data]);

useEffect(() => {
if (!editorRef?.selection) {
return;
}

localState.selection = { ...editorRef?.selection };
}, [editorRef?.selection]);

const panel = useSQLCodeEditorPanel(data, editor);
const extensions = useCodemirrorExtensions(undefined, [ACTIVE_QUERY_EXTENSION, Prec.lowest(QUERY_STATUS_GUTTER_EXTENSION)]);
const autocompletion = useSqlDialectAutocompletion(data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import React from 'react';

import { importLazyComponent } from '@cloudbeaver/core-blocks';
import { injectable } from '@cloudbeaver/core-di';
import { ESqlDataSourceFeatures, SqlEditorModeService } from '@cloudbeaver/plugin-sql-editor';

const SQLCodeEditorPanel = React.lazy(async () => {
const { SQLCodeEditorPanel } = await import('./SQLCodeEditorPanel');
return { default: SQLCodeEditorPanel };
});
const SQLCodeEditorPanel = importLazyComponent(() => import('./SQLCodeEditorPanel').then(module => module.SQLCodeEditorPanel));

@injectable()
export class SQLCodeEditorPanelService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { useCallback } from 'react';

import { useExecutor, useObservableRef } from '@cloudbeaver/core-blocks';
import { throttle } from '@cloudbeaver/core-utils';
import type { Transaction, ViewUpdate } from '@cloudbeaver/plugin-codemirror6';
import type { ISQLEditorData } from '@cloudbeaver/plugin-sql-editor';

import type { IEditor } from '../SQLCodeEditor/useSQLCodeEditor';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { IDatabaseDataModel, IDatabaseResultSet } from '@cloudbeaver/plugin

import type { IDataQueryOptions } from '../QueryDataSource';
import { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures';
import type { ISetScriptData, ISqlDataSource, ISqlDataSourceKey } from './ISqlDataSource';
import type { ISetScriptData, ISqlDataSource, ISqlDataSourceKey, ISqlEditorCursor } from './ISqlDataSource';
import type { ISqlDataSourceHistory } from './SqlDataSourceHistory/ISqlDataSourceHistory';
import { SqlDataSourceHistory } from './SqlDataSourceHistory/SqlDataSourceHistory';

Expand All @@ -37,6 +37,10 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
incomingExecutionContext: IConnectionExecutionContextInfo | undefined | null;
exception?: Error | Error[] | null | undefined;

get cursor(): ISqlEditorCursor {
return this.innerCursorState;
}

get isIncomingChanges(): boolean {
return this.incomingScript !== undefined || this.incomingExecutionContext !== null;
}
Expand Down Expand Up @@ -81,6 +85,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {

protected outdated: boolean;
protected editing: boolean;
protected innerCursorState: ISqlEditorCursor;

constructor(icon = '/icons/sql_script_m.svg') {
this.icon = icon;
Expand All @@ -91,6 +96,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
this.message = undefined;
this.outdated = true;
this.editing = true;
this.innerCursorState = { begin: 0, end: 0 };
this.history = new SqlDataSourceHistory();
this.onUpdate = new SyncExecutor();
this.onSetScript = new SyncExecutor();
Expand All @@ -107,7 +113,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {

this.history.onNavigate.addHandler(value => this.setScript(value, SOURCE_HISTORY));

makeObservable<this, 'outdated' | 'editing'>(this, {
makeObservable<this, 'outdated' | 'editing' | 'innerCursorState'>(this, {
isSaved: computed,
isIncomingChanges: computed,
isAutoSaveEnabled: computed,
Expand All @@ -130,6 +136,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
outdated: observable.ref,
message: observable.ref,
editing: observable.ref,
innerCursorState: observable.ref,
incomingScript: observable.ref,
incomingExecutionContext: observable.ref,
});
Expand Down Expand Up @@ -225,6 +232,20 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
return this.features.includes(feature);
}

setCursor(begin: number, end = begin): void {
if (begin > end) {
throw new Error('Cursor begin can not be greater than the end of it');
}

const scriptLength = this.script.length;

this.innerCursorState = Object.freeze({
begin: Math.min(begin, scriptLength),
end: Math.min(end, scriptLength),
});
this.onUpdate.execute();
}

setEditing(state: boolean): void {
this.editing = state;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export interface ISetScriptData {
source?: string;
}

export interface ISqlEditorCursor {
readonly begin: number;
readonly end: number;
}

export interface ISqlDataSource extends ILoadableState {
readonly name: string | null;
readonly icon?: string;
Expand All @@ -33,6 +38,7 @@ export interface ISqlDataSource extends ILoadableState {
readonly projectId: string | null;

readonly script: string;
readonly cursor: ISqlEditorCursor;
readonly incomingScript?: string;
readonly history: ISqlDataSourceHistory;

Expand Down Expand Up @@ -65,6 +71,7 @@ export interface ISqlDataSource extends ILoadableState {
setName(name: string | null): void;
setProject(projectId: string | null): void;
setScript(script: string, source?: string): void;
setCursor(begin: number, end?: number): void;
setEditing(state: boolean): void;
setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void;
setIncomingExecutionContext(executionContext?: IConnectionExecutionContextInfo): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import type { ISyncExecutor } from '@cloudbeaver/core-executor';
import type { SqlDialectInfo } from '@cloudbeaver/core-sdk';

import type { ISqlDataSource } from '../SqlDataSource/ISqlDataSource';
import type { ISqlDataSource, ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource';
import type { SQLProposal } from '../SqlEditorService';
import type { ISQLScriptSegment, SQLParser } from '../SQLParser';
import type { ISQLEditorMode } from './SQLEditorModeContext';
Expand All @@ -18,13 +18,8 @@ export interface ISegmentExecutionData {
type: 'start' | 'end' | 'error';
}

export interface ICursor {
readonly begin: number;
readonly end: number;
}

export interface ISQLEditorData {
readonly cursor: ICursor;
readonly cursor: ISqlEditorCursor;
activeSegmentMode: ISQLEditorMode;
readonly parser: SQLParser;
readonly dialect: SqlDialectInfo | undefined;
Expand Down
Loading
Loading