Skip to content

Commit

Permalink
feat(supersearch): Insert space before qualifier (LWS-286) (#1198)
Browse files Browse the repository at this point in the history
* fix(supersearch): Insert space before qualifier
* build separate atomic rangeset
* Handle space input before qualifier
* Name file same as function
  • Loading branch information
jesperengstrom authored Jan 15, 2025
1 parent 0878806 commit 666663d
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 47 deletions.
87 changes: 40 additions & 47 deletions packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {
WidgetType,
type DecorationSet
} from '@codemirror/view';
import { EditorState, type Range } from '@codemirror/state';
import { EditorState, Range, RangeSet, RangeSetBuilder, RangeValue } from '@codemirror/state';
import { syntaxTree } from '@codemirror/language';
import { mount } from 'svelte';
import QualifierComponent from './QualifierComponent.svelte';
import insertQuotes from './insertQuotes.js';
import { messages } from '$lib/constants/messages.js';
import insertSpaceBeforeQualifier from './insertSpaceBeforeQualifier.js';

export type Qualifier = {
key: string;
Expand All @@ -38,8 +39,7 @@ class QualifierWidget extends WidgetType {
readonly valueLabel: string | undefined,
readonly operator: string,
readonly operatorType: string | undefined,
readonly removeLink: string | undefined,
readonly atomic: boolean
readonly removeLink: string | undefined
) {
super();
}
Expand Down Expand Up @@ -73,8 +73,11 @@ class QualifierWidget extends WidgetType {
}

function lxlQualifierPlugin(getLabelFn?: GetLabelFunction) {
let atomicRangeSet: RangeSet<RangeValue> = RangeSet.empty;

function getQualifiers(view: EditorView) {
const widgets: Range<Decoration>[] = [];
const ranges = new RangeSetBuilder();
const doc = view.state.doc.toString();

for (const { from, to } of view.visibleRanges) {
Expand Down Expand Up @@ -107,19 +110,19 @@ function lxlQualifierPlugin(getLabelFn?: GetLabelFunction) {
valueLabel,
operator,
operatorType,
removeLink,
true // atomic
removeLink
)
});
const decorationRangeFrom = node.from;
const decorationRangeTo = valueLabel ? node.to : operatorNode?.to;

ranges.add(decorationRangeFrom, decorationRangeTo || node.to, qualifierDecoration);
widgets.push(qualifierDecoration.range(decorationRangeFrom, decorationRangeTo));
} else if (invalid) {
// Add invalid key mark decoration
const qualifierMark = Decoration.mark({
class: 'invalid',
inclusive: true,
atomic: false
inclusive: true
});
const invalidRangeFrom = keyNode ? keyNode.from : node.from;
const invalidRangeTo = keyNode ? keyNode.to : operatorNode?.from;
Expand All @@ -130,53 +133,43 @@ function lxlQualifierPlugin(getLabelFn?: GetLabelFunction) {
}
});
}
return Decoration.set(widgets, true); // true = sort
atomicRangeSet = ranges.finish();
return Decoration.set(widgets, true);
}

/**
* filter out non-atomics using custom property 'atomic'
*/
const filterAtomic = (from: number, to: number, decoration: Decoration) => {
return decoration.spec?.atomic || decoration.spec?.widget?.atomic;
};

const qualifierPlugin = ViewPlugin.fromClass(
class {
qualifiers: DecorationSet;
constructor(view: EditorView) {
this.qualifiers = getQualifiers(view);
}

update(update: ViewUpdate) {
if (update.docChanged || syntaxTree(update.startState) != syntaxTree(update.state)) {
// TODO: Calling getQualifiers on every document change is probably not good for performance
// Try optimizing; either run the function only on certain kinds of input, or split getQualifiers;
// one that updates the widgets (on input) and one that looks for labels (on data update)
this.qualifiers = getQualifiers(update.view);
} else {
for (const tr of update.transactions) {
for (const e of tr.effects) {
if (e.value.message === messages.NEW_DATA) {
this.qualifiers = getQualifiers(update.view);
}
class LxlQualifier {
qualifiers: DecorationSet;
constructor(view: EditorView) {
this.qualifiers = getQualifiers(view);
}
update(update: ViewUpdate) {
if (update.docChanged || syntaxTree(update.startState) != syntaxTree(update.state)) {
// TODO: Calling getQualifiers on every document change is probably not good for performance
// Try optimizing; either run the function only on certain kinds of input, or split getQualifiers;
// one that updates the widgets (on input) and one that looks for labels (on data update)
this.qualifiers = getQualifiers(update.view);
} else {
for (const tr of update.transactions) {
for (const e of tr.effects) {
if (e.value.message === messages.NEW_DATA) {
this.qualifiers = getQualifiers(update.view);
}
}
}
}
},
{
decorations: (instance) => instance.qualifiers,
eventHandlers: {},
provide: (plugin) => [
EditorView.atomicRanges.of((view) => {
const filteredRanges = view.plugin(plugin)?.qualifiers.update({ filter: filterAtomic });
return filteredRanges || Decoration.none;
}),
EditorState.transactionFilter.of(insertQuotes)
]
}
);
return qualifierPlugin;
}

const plugin = ViewPlugin.fromClass(LxlQualifier, {
decorations: (instance) => instance.qualifiers,
provide: () => [
EditorView.atomicRanges.of(() => atomicRangeSet),
EditorState.transactionFilter.of(insertQuotes),
insertSpaceBeforeQualifier(() => atomicRangeSet)
]
});

return plugin;
}

export default lxlQualifierPlugin;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { EditorState, RangeSet, RangeValue } from '@codemirror/state';

const insertSpaceBeforeQualifier = (getRanges: () => RangeSet<RangeValue>) => {
return EditorState.transactionFilter.of((tr) => {
if (!tr.docChanged || (tr.isUserEvent('delete') && tr.state.selection.main.head === 0)) {
return tr;
} else {
let insert = {};
let changes = {};
const atomicRanges = getRanges();
const oldCursorPos = tr.startState.selection.main.head;
const newCursorPos = tr.state.selection.main.head;

atomicRanges.between(oldCursorPos, oldCursorPos, (from) => {
if (oldCursorPos === from) {
// overlap is at atomic range start
const input = !!tr.newDoc.slice(oldCursorPos, newCursorPos).toString().trim();
const isDelete = tr.isUserEvent('delete');
// don't add space if input is space
// instead, move cursor back to old pos
if (input || isDelete) {
changes = {
from: newCursorPos,
to: newCursorPos,
insert: ' '
};
}
insert = {
changes,
sequential: true,
selection: { anchor: input || isDelete ? newCursorPos : oldCursorPos }
};
}
return false;
});
return [tr, insert];
}
});
};

export default insertSpaceBeforeQualifier;

0 comments on commit 666663d

Please sign in to comment.