66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
|
|
/**
|
||
|
|
* Snippet completion source. Surfaces custom snippets in terminal autocomplete
|
||
|
|
* when the user is typing the command name. Matches against the snippet label
|
||
|
|
* and the first line of its command (case-insensitive; prefix matches rank
|
||
|
|
* above substring matches). Chinese labels also match via pinyin / initials
|
||
|
|
* through the shared search matcher (#2813). Each suggestion carries the full
|
||
|
|
* Snippet so the accept path can run it through the canonical executeSnippetCommand.
|
||
|
|
*/
|
||
|
|
import type { Snippet } from "../../../domain/models";
|
||
|
|
import { snippetAppliesToHost } from "../../../domain/snippetTargets";
|
||
|
|
import { matchesSearchQuery } from "../../../lib/searchMatcher";
|
||
|
|
import type { CompletionSuggestion } from "./completionEngine";
|
||
|
|
|
||
|
|
const SNIPPET_BASE_SCORE = 2000; // Above history (1000+freq) per "snippet > history".
|
||
|
|
const SNIPPET_PREFIX_BONUS = 100;
|
||
|
|
|
||
|
|
function snippetAvailableForAutocomplete(
|
||
|
|
snippet: Snippet,
|
||
|
|
host: { hostId?: string; hostGroup?: string },
|
||
|
|
): boolean {
|
||
|
|
if (snippet.targetsAllHosts) return true;
|
||
|
|
const hasScopedTargets = Boolean(
|
||
|
|
snippet.targets?.length || snippet.targetGroups !== undefined,
|
||
|
|
);
|
||
|
|
if (!hasScopedTargets) return true;
|
||
|
|
if (!host.hostId) return false;
|
||
|
|
return snippetAppliesToHost(snippet, { id: host.hostId, group: host.hostGroup });
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getSnippetSuggestions(
|
||
|
|
input: string,
|
||
|
|
snippets: Snippet[],
|
||
|
|
options: { hostId?: string; hostGroup?: string } = {},
|
||
|
|
): CompletionSuggestion[] {
|
||
|
|
const needle = input.trim().toLowerCase();
|
||
|
|
if (!needle || !Array.isArray(snippets)) return [];
|
||
|
|
|
||
|
|
const out: CompletionSuggestion[] = [];
|
||
|
|
for (const snippet of snippets) {
|
||
|
|
if (!snippetAvailableForAutocomplete(snippet, options)) continue;
|
||
|
|
const label = (snippet.label || "").toLowerCase();
|
||
|
|
const firstLine = (snippet.command || "").split("\n")[0].trim().toLowerCase();
|
||
|
|
|
||
|
|
const labelPrefix = label.startsWith(needle);
|
||
|
|
// Literal prefix/substring first (cheap); fall back to shared smart matcher
|
||
|
|
// so Chinese titles surface for pinyin / initials the same way host search does.
|
||
|
|
const matches = labelPrefix
|
||
|
|
|| label.includes(needle)
|
||
|
|
|| firstLine.startsWith(needle)
|
||
|
|
|| matchesSearchQuery(needle, snippet.label, firstLine);
|
||
|
|
if (!matches) continue;
|
||
|
|
|
||
|
|
out.push({
|
||
|
|
text: snippet.label,
|
||
|
|
displayText: snippet.label,
|
||
|
|
description: snippet.command,
|
||
|
|
source: "snippet",
|
||
|
|
score: SNIPPET_BASE_SCORE + (labelPrefix ? SNIPPET_PREFIX_BONUS : 0),
|
||
|
|
snippet,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
out.sort((a, b) => b.score - a.score);
|
||
|
|
return out;
|
||
|
|
}
|