mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-31 08:46:58 +02:00
fix(editor): Place caret inside expression brackets on auto-switch (#30030)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Michael Kret <michael.k@radency.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com>
This commit is contained in:
parent
c730def839
commit
df5a1c4452
|
|
@ -75,6 +75,7 @@ const {
|
|||
isReadOnly: computed(() => props.isReadOnly),
|
||||
autocompleteTelemetry: { enabled: true, parameterPath: props.path },
|
||||
additionalData: props.additionalData,
|
||||
initialCursorPosition: 'lastExpression',
|
||||
});
|
||||
|
||||
watch(segments.display, (newSegments) => {
|
||||
|
|
@ -102,8 +103,10 @@ defineExpose({
|
|||
setCursorPosition,
|
||||
focus: () => {
|
||||
if (!hasFocus.value) {
|
||||
setCursorPosition('lastExpression');
|
||||
focus();
|
||||
requestAnimationFrame(() => {
|
||||
setCursorPosition('lastExpression');
|
||||
});
|
||||
}
|
||||
},
|
||||
selectAll: () => {
|
||||
|
|
|
|||
|
|
@ -297,6 +297,58 @@ describe('useExpressionEditor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('initialCursorPosition', () => {
|
||||
test('should place cursor inside the empty expression block when value is auto-converted', async () => {
|
||||
const editorValue = 'Hello {{ }}';
|
||||
const expectedPosition = editorValue.lastIndexOf(' }}');
|
||||
const {
|
||||
expressionEditor: { editor },
|
||||
} = await renderExpressionEditor({
|
||||
editorValue,
|
||||
initialCursorPosition: 'lastExpression',
|
||||
extensions: [n8nLang()],
|
||||
});
|
||||
|
||||
expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(expectedPosition));
|
||||
});
|
||||
|
||||
test('should place cursor at end when option is "end"', async () => {
|
||||
const editorValue = 'text here';
|
||||
const {
|
||||
expressionEditor: { editor },
|
||||
} = await renderExpressionEditor({
|
||||
editorValue,
|
||||
initialCursorPosition: 'end',
|
||||
});
|
||||
|
||||
expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(editorValue.length));
|
||||
});
|
||||
|
||||
test('should place cursor at the given numeric position', async () => {
|
||||
const editorValue = 'text here';
|
||||
const {
|
||||
expressionEditor: { editor },
|
||||
} = await renderExpressionEditor({
|
||||
editorValue,
|
||||
initialCursorPosition: 3,
|
||||
});
|
||||
|
||||
expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(3));
|
||||
});
|
||||
|
||||
test('should default to position 0 when no option is provided', async () => {
|
||||
const editorValue = 'Hello {{ }}';
|
||||
const {
|
||||
expressionEditor: { editor },
|
||||
} = await renderExpressionEditor({
|
||||
editorValue,
|
||||
extensions: [n8nLang()],
|
||||
});
|
||||
|
||||
expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('select()', () => {
|
||||
test('should select number range', async () => {
|
||||
const editorValue = 'text here';
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export const useExpressionEditor = ({
|
|||
autocompleteTelemetry,
|
||||
isReadOnly = false,
|
||||
disableSearchDialog = false,
|
||||
initialCursorPosition,
|
||||
onChange = () => {},
|
||||
}: {
|
||||
editorRef: MaybeRefOrGetter<HTMLElement | undefined>;
|
||||
|
|
@ -77,6 +78,7 @@ export const useExpressionEditor = ({
|
|||
autocompleteTelemetry?: MaybeRefOrGetter<{ enabled: true; parameterPath: string }>;
|
||||
isReadOnly?: MaybeRefOrGetter<boolean>;
|
||||
disableSearchDialog?: MaybeRefOrGetter<boolean>;
|
||||
initialCursorPosition?: number | 'lastExpression' | 'end';
|
||||
onChange?: (viewUpdate: ViewUpdate) => void;
|
||||
}) => {
|
||||
const ndvStore = useNDVStore();
|
||||
|
|
@ -233,13 +235,33 @@ export const useExpressionEditor = ({
|
|||
}
|
||||
}
|
||||
|
||||
function resolveInitialCursorPosition(
|
||||
doc: string,
|
||||
pos: number | 'lastExpression' | 'end',
|
||||
): number {
|
||||
if (typeof pos === 'number') return pos;
|
||||
if (pos === 'end') return doc.length;
|
||||
const END_OF_EXPRESSION = ' }}';
|
||||
const endOfLastExpression = doc.lastIndexOf(END_OF_EXPRESSION);
|
||||
return endOfLastExpression !== -1 ? endOfLastExpression : doc.length;
|
||||
}
|
||||
|
||||
watch(toRef(editorRef), () => {
|
||||
const parent = toValue(editorRef);
|
||||
|
||||
if (!parent) return;
|
||||
|
||||
const docContent = toValue(editorValue) ?? '';
|
||||
const initialSelection =
|
||||
initialCursorPosition !== undefined
|
||||
? EditorSelection.cursor(resolveInitialCursorPosition(docContent, initialCursorPosition))
|
||||
: undefined;
|
||||
|
||||
let hasReceivedFocus = false;
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: toValue(editorValue),
|
||||
doc: docContent,
|
||||
selection: initialSelection,
|
||||
extensions: [
|
||||
TARGET_NODE_PARAMETER_FACET.of(
|
||||
expressionLocalResolveContext.value
|
||||
|
|
@ -251,9 +273,15 @@ export const useExpressionEditor = ({
|
|||
readOnlyExtensions.value.of([EditorState.readOnly.of(toValue(isReadOnly))]),
|
||||
telemetryExtensions.value.of([]),
|
||||
EditorView.updateListener.of(onEditorUpdate),
|
||||
EditorView.focusChangeEffect.of((_, newHasFocus) => {
|
||||
EditorView.focusChangeEffect.of((currentState, newHasFocus) => {
|
||||
hasFocus.value = newHasFocus;
|
||||
selection.value = state.selection.ranges[0];
|
||||
selection.value = currentState.selection.ranges[0];
|
||||
if (newHasFocus && !hasReceivedFocus && initialSelection) {
|
||||
hasReceivedFocus = true;
|
||||
requestAnimationFrame(() => {
|
||||
editor.value?.dispatch({ selection: initialSelection });
|
||||
});
|
||||
}
|
||||
if (!newHasFocus) {
|
||||
autocompleteStatus.value = null;
|
||||
void debouncedUpdateSegments();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user