fluxer/fluxer_app/src/utils/SearchSegmentManager.ts
2026-01-01 21:05:54 +00:00

170 lines
4.6 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
type SearchSegmentType = 'user' | 'channel' | 'value';
export interface SearchSegment {
type: SearchSegmentType;
filterKey: string;
id: string;
displayText: string;
start: number;
end: number;
}
export class SearchSegmentManager {
private segments: Array<SearchSegment> = [];
getSegments(): Array<SearchSegment> {
return [...this.segments];
}
setSegments(segments: Array<SearchSegment>): void {
this.segments = [...segments];
}
clear(): void {
this.segments = [];
}
getSegmentAt(position: number): SearchSegment | null {
return this.segments.find((seg) => seg.start <= position && seg.end >= position) || null;
}
updateSegmentsForTextChange(changeStart: number, changeEnd: number, replacementLength: number): Array<SearchSegment> {
const lengthDelta = replacementLength - (changeEnd - changeStart);
const updated = this.segments
.map((segment) => {
if (segment.end <= changeStart) {
return segment;
} else if (segment.start >= changeEnd) {
return {...segment, start: segment.start + lengthDelta, end: segment.end + lengthDelta};
} else {
return null;
}
})
.filter((s): s is SearchSegment => s !== null);
this.segments = updated;
return updated;
}
insertSegment(
currentText: string,
insertPosition: number,
filterKey: string,
filterSyntax: string,
displayText: string,
id: string,
type: SearchSegmentType,
): {newText: string; newSegments: Array<SearchSegment>} {
const fullDisplayText = `${filterSyntax}${displayText}`;
const newText = `${currentText.slice(0, insertPosition) + fullDisplayText} ${currentText.slice(insertPosition)}`;
const newSegment: SearchSegment = {
type,
filterKey,
id,
displayText: fullDisplayText,
start: insertPosition,
end: insertPosition + fullDisplayText.length,
};
const updatedSegments = this.segments.map((segment) => {
if (segment.start >= insertPosition) {
const offset = fullDisplayText.length + 1;
return {
...segment,
start: segment.start + offset,
end: segment.end + offset,
};
}
return segment;
});
this.segments = [...updatedSegments, newSegment];
return {newText, newSegments: this.segments};
}
replaceWithSegment(
currentText: string,
replaceStart: number,
replaceEnd: number,
filterKey: string,
filterSyntax: string,
displayText: string,
id: string,
type: SearchSegmentType,
): {newText: string; newSegments: Array<SearchSegment>} {
const fullDisplayText = `${filterSyntax}${displayText}`;
const lengthDelta = fullDisplayText.length + 1 - (replaceEnd - replaceStart);
const newText = `${currentText.slice(0, replaceStart) + fullDisplayText} ${currentText.slice(replaceEnd)}`;
const newSegment: SearchSegment = {
type,
filterKey,
id,
displayText: fullDisplayText,
start: replaceStart,
end: replaceStart + fullDisplayText.length,
};
const updatedSegments = this.segments
.map((segment) => {
if (segment.end <= replaceStart) {
return segment;
} else if (segment.start >= replaceEnd) {
return {...segment, start: segment.start + lengthDelta, end: segment.end + lengthDelta};
} else {
return null;
}
})
.filter((s): s is SearchSegment => s !== null);
this.segments = [...updatedSegments, newSegment];
return {newText, newSegments: this.segments};
}
static detectChange(
oldText: string,
newText: string,
): {changeStart: number; changeEnd: number; replacementLength: number} {
let changeStart = 0;
while (
changeStart < oldText.length &&
changeStart < newText.length &&
oldText[changeStart] === newText[changeStart]
) {
changeStart++;
}
let oldEnd = oldText.length;
let newEnd = newText.length;
while (oldEnd > changeStart && newEnd > changeStart && oldText[oldEnd - 1] === newText[newEnd - 1]) {
oldEnd--;
newEnd--;
}
const replacementLength = newEnd - changeStart;
return {changeStart, changeEnd: oldEnd, replacementLength};
}
}