@pierre/diffs
Version:
319 lines (318 loc) • 8.7 kB
JavaScript
import { buildSearchReplacementText } from "./pieceTable.js";
import { isPrimaryModifier } from "./platform.js";
import { resolveFindAgainShortcut } from "./command.js";
import { h } from "./utils.js";
import { getEditorIconSvg } from "./sprite.js";
//#region src/editor/searchPanel.ts
var SearchPanelWidget = class {
#container;
#inputElement;
#updateMatches;
#applyMode;
#navigate;
#close;
constructor(options) {
const { textDocument, containerElement, defaultQuery, mode = "find", initialMatch, scrollToMatch, applyReplace, onUpdate, onClose } = options;
const searchParams = {
text: defaultQuery,
replaceText: "",
caseSensitive: false,
wholeWord: false,
regex: false
};
const matches = {
all: [],
current: void 0
};
const matchResultElement = h("div", {
dataset: {
matches: "",
noMatches: ""
},
textContent: "No results"
});
const updateMatches = (options) => {
matches.all = searchParams.text !== "" ? textDocument.search(searchParams) : [];
const noMatches = matches.all.length === 0;
prevButton.disabled = noMatches;
nextButton.disabled = noMatches;
if (noMatches) {
matchResultElement.textContent = "No results";
matchResultElement.dataset.noMatches = "";
matches.current = void 0;
onUpdate([]);
return;
}
delete matchResultElement.dataset.noMatches;
if (options?.syncSelection === false) {
updateCurrentMatch(onUpdate(matches.all, { syncSelection: false }));
return;
}
updateCurrentMatch(onUpdate(matches.all));
};
this.#updateMatches = updateMatches;
const updateCurrentMatch = (currentMatch) => {
if (currentMatch === void 0) matchResultElement.textContent = `${matches.all.length} results`;
else {
const [start, end] = currentMatch;
matchResultElement.textContent = `${matches.all.findIndex((m) => m[0] === start && m[1] === end) + 1} of ${matches.all.length}`;
}
matches.current = currentMatch;
};
const updateSearchParam = (key, value) => {
searchParams[key] = value;
updateMatches();
};
const findNextMatch = (findPrevious = false, retainFocus = false) => {
const allMatches = matches.all;
let nextMatch = allMatches[0];
if (allMatches.length > 0) if (findPrevious) {
const searchOffset = matches.current?.[0] ?? 0;
nextMatch = allMatches.at(-1);
for (const m of allMatches) if (m[1] <= searchOffset) nextMatch = m;
else break;
} else {
const searchOffset = matches.current?.[1] ?? 0;
for (const m of allMatches) if (m[0] >= searchOffset) {
nextMatch = m;
break;
}
}
if (nextMatch !== void 0) {
updateCurrentMatch(nextMatch);
scrollToMatch(nextMatch, retainFocus);
}
matches.current = nextMatch;
};
this.#navigate = (findPrevious) => findNextMatch(findPrevious, true);
const buildReplacementEdit = (matchStart, matchEnd) => ({
start: matchStart,
end: matchEnd,
text: buildSearchReplacementText((offset) => textDocument.positionAt(offset), (position) => textDocument.offsetAt(position), (line) => textDocument.getLineText(line), searchParams, matchStart, matchEnd)
});
const replace = () => {
if (searchParams.text === "" || matches.all.length === 0) return;
let currentMatch = matches.current;
if (currentMatch === void 0) {
findNextMatch(false, true);
currentMatch = matches.current;
if (currentMatch === void 0) return;
}
const [start, end] = currentMatch;
const edit = buildReplacementEdit(start, end);
applyReplace([edit]);
scrollToMatch([start + edit.text.length, start + edit.text.length], true);
matches.current = void 0;
updateMatches();
};
const replaceAll = () => {
if (searchParams.text === "" || matches.all.length === 0) return;
applyReplace(matches.all.map(([start, end]) => buildReplacementEdit(start, end)));
matches.current = void 0;
updateMatches();
};
const close = () => {
this.cleanup();
onClose();
};
this.#close = close;
const iconButton = (opts) => h("button", {
type: "button",
title: opts.label,
ariaLabel: opts.label,
dataset: {
searchIcon: "",
...opts.dataset
},
innerHTML: getEditorIconSvg(opts.icon, opts.size ?? 16),
onclick: opts.onClick
});
const makeToggle = (icon, title, key) => {
const button = iconButton({
icon,
label: title,
size: 14,
onClick: () => {
const next = !searchParams[key];
button.ariaPressed = String(next);
updateSearchParam(key, next);
}
});
button.ariaPressed = String(searchParams[key]);
return button;
};
const caseSensitiveToggle = makeToggle("case", "Match Case", "caseSensitive");
const wholeWordToggle = makeToggle("whole-word", "Whole Word", "wholeWord");
const regexToggle = makeToggle("regex", "Regexp", "regex");
const replaceInputElement = h("input", {
type: "text",
placeholder: "Replace",
dataset: "replace",
value: "",
oninput: (e) => {
searchParams.replaceText = e.target.value;
},
onkeydown: (e) => {
if (e.isComposing || e.keyCode === 229) return;
const findAgain = resolveFindAgainShortcut(e);
if (e.key === "Escape") {
e.preventDefault();
close();
} else if (e.key === "Enter") {
e.preventDefault();
replace();
} else if (findAgain !== void 0) {
e.preventDefault();
findNextMatch(findAgain === "previous", true);
}
}
});
this.#inputElement = h("input", {
type: "text",
placeholder: "Search",
dataset: "search",
value: defaultQuery,
oninput: (e) => {
searchParams.text = e.target.value;
matches.current = void 0;
updateMatches();
},
onkeydown: (e) => {
if (e.isComposing || e.keyCode === 229) return;
const findAgain = resolveFindAgainShortcut(e);
if (e.key === "Escape") {
e.preventDefault();
close();
} else if (e.key === "Enter") {
e.preventDefault();
findNextMatch(e.shiftKey, true);
} else if (findAgain !== void 0) {
e.preventDefault();
findNextMatch(findAgain === "previous", true);
} else if (isPrimaryModifier(e) && (e.key === "f" || e.code === "KeyF")) {
e.preventDefault();
applyMode(e.altKey ? "replace" : "find");
}
}
});
const searchTogglesElement = h("div", {
dataset: "searchToggles",
children: [
caseSensitiveToggle,
wholeWordToggle,
regexToggle
]
});
const findInputBox = h("div", {
dataset: {
inputBox: "",
find: ""
},
children: [this.#inputElement, searchTogglesElement]
});
const replaceInputBox = h("div", {
dataset: {
inputBox: "",
replace: "",
replaceCell: ""
},
children: [replaceInputElement]
});
const replaceActionsElement = h("div", {
dataset: {
replaceActions: "",
replaceCell: ""
},
children: [iconButton({
icon: "replace",
label: "Replace",
onClick: replace
}), iconButton({
icon: "replace-all",
label: "Replace All",
onClick: replaceAll
})]
});
const prevButton = iconButton({
icon: "arrow-up",
label: "Previous",
onClick: () => {
findNextMatch(true);
}
});
const nextButton = iconButton({
icon: "arrow-down",
label: "Next",
onClick: () => {
findNextMatch();
}
});
prevButton.disabled = true;
nextButton.disabled = true;
const navElement = h("div", {
dataset: "searchNav",
children: [prevButton, nextButton]
});
const closeElement = iconButton({
icon: "close",
label: "Close",
onClick: close,
dataset: { searchClose: "" }
});
const gridElement = h("div", {
dataset: {
searchGrid: "",
mode
},
children: [
findInputBox,
replaceInputBox,
replaceActionsElement,
matchResultElement,
navElement,
closeElement
]
});
const applyMode = (next) => {
gridElement.dataset.mode = next;
this.#inputElement.focus();
this.#inputElement.select();
};
this.#applyMode = applyMode;
this.#container = h("div", {
dataset: "searchPanel",
children: [h("div", {
dataset: "editorWidget",
children: [gridElement]
})]
});
matches.current = initialMatch;
containerElement.before(this.#container);
requestAnimationFrame(() => {
if (initialMatch !== void 0) updateMatches();
else onUpdate([]);
this.#inputElement.select();
});
}
focus() {
this.#inputElement.focus();
}
navigate(findPrevious) {
this.#navigate?.(findPrevious);
}
updateMatches(options) {
this.#updateMatches?.(options);
}
setMode(mode) {
this.#applyMode?.(mode);
}
close() {
this.#close?.();
}
cleanup() {
this.#container.remove();
}
};
//#endregion
export { SearchPanelWidget };
//# sourceMappingURL=searchPanel.js.map