Skip to content
Source
Sign inSign up

usePullRequestCompletions.ts

82 lines3.3 KB

1import { EnvironmentId, ProjectId } from "@t3tools/contracts";
2import {
3 debouncedPullRequestCompletionSearch,
4 detectPullRequestComposerTrigger,
5 insertPullRequestCompletion,
6 pullRequestCompletionKey,
7 pullRequestCompletions,
8 type PullRequestCompletion,
9 type PullRequestCompletionSupport,
10} from "@t3tools/shared/pullRequestComposerTrigger";
11import { useState } from "react";
12import type { TextInputSelectionChangeEvent } from "react-native";
13​
14import { composerPullRequests } from "../../state/pull-requests";
15import { useEnvironmentQuery } from "../../state/query";
16import { useDebouncedValue } from "../../state/queries";
17​
18const COMPLETION_SUGGESTION_LIMIT = 6;
19​
20/** Completes `@login`, `#123` and GitLab `!123` in a pull request text input. */
21export function usePullRequestCompletions(input: {
22 readonly environmentId: string;
23 readonly projectId: string;
24 readonly value: string;
25 readonly onChangeValue: (value: string) => void;
26 /** What the pull request's host completes (its `capabilities`); null where the field takes none. */
27 readonly support: PullRequestCompletionSupport | null;
28}) {
29 const [selection, setSelection] = useState<{ start: number; end: number } | null>(null);
30 // Set only right after an insertion, so the native input keeps its own caret otherwise.
31 const [placed, setPlaced] = useState<{ start: number; end: number } | null>(null);
32 const trigger =
33 input.support === null || selection === null || selection.start !== selection.end
34 ? null
35 : detectPullRequestComposerTrigger(input.value, selection.start, input.support);
36 const key = pullRequestCompletionKey(trigger);
37 const sentKey = useDebouncedValue(key, 250);
38 const sent =
39 trigger === null ? undefined : debouncedPullRequestCompletionSearch(trigger, sentKey);
40 const hosted = useEnvironmentQuery(
41 trigger === null
42 ? null
43 : composerPullRequests.filterOptions({
44 environmentId: EnvironmentId.make(input.environmentId),
45 input:
46 trigger.kind === "mention"
47 ? {
48 kind: "authors",
49 projectId: ProjectId.make(input.projectId),
50 ...(sent ? { query: sent } : {}),
51 }
52 : {
53 kind: "references",
54 projectId: ProjectId.make(input.projectId),
55 sigil: trigger.sigil,
56 ...(sent ? { query: sent } : {}),
57 },
58 }),
59 );
60 // Older answers stay useful while the debounced search catches up, narrowed to what is typed now.
61 const candidates = pullRequestCompletions(trigger, hosted.data, COMPLETION_SUGGESTION_LIMIT);
62​
63 return {
64 kind: trigger?.kind ?? null,
65 candidates,
66 isLoading: hosted.isPending || sentKey !== key,
67 failed: hosted.error !== null,
68 selection: placed ?? undefined,
69 onSelectionChange: (event: TextInputSelectionChangeEvent) => {
70 setSelection(event.nativeEvent.selection);
71 setPlaced(null);
72 },
73 onBlur: () => setSelection(null),
74 select: (completion: PullRequestCompletion) => {
75 if (trigger === null) return;
76 const next = insertPullRequestCompletion(input.value, trigger, completion.token);
77 input.onChangeValue(next.text);
78 setSelection({ start: next.cursor, end: next.cursor });
79 setPlaced({ start: next.cursor, end: next.cursor });
80 },
81 };