Skip to content

Commit d72b7d7

Browse files
authored
Fix URL autocomplete replacement (#510)
1 parent b40e2cd commit d72b7d7

5 files changed

Lines changed: 110 additions & 31 deletions

File tree

apps/yaak-client/components/HttpRequestPane.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { BinaryFileEditor } from "./BinaryFileEditor";
3939
import { ConfirmLargeRequestBody } from "./ConfirmLargeRequestBody";
4040
import { CountBadge } from "./core/CountBadge";
4141
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
42+
import { getUrlCompletionConfig } from "./core/Editor/url/completion";
4243
import { Editor } from "./core/Editor/LazyEditor";
4344
import { InlineCode } from "@yaakapp-internal/ui";
4445
import type { Pair } from "./core/PairEditor";
@@ -285,16 +286,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
285286
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
286287

287288
const autocomplete: GenericCompletionConfig = useMemo(
288-
() => ({
289-
minMatch: 3,
290-
options:
291-
autocompleteUrls.length > 0
292-
? autocompleteUrls
293-
: [
294-
{ label: "http://", type: "constant" },
295-
{ label: "https://", type: "constant" },
296-
],
297-
}),
289+
() => getUrlCompletionConfig(autocompleteUrls),
298290
[autocompleteUrls],
299291
);
300292

apps/yaak-client/components/WebsocketRequestPane.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { prepareImportQuerystring } from "../lib/prepareImportQuerystring";
2626
import { resolvedModelName } from "../lib/resolvedModelName";
2727
import { CountBadge } from "./core/CountBadge";
2828
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
29+
import { getUrlCompletionConfig } from "./core/Editor/url/completion";
2930
import { Editor } from "./core/Editor/LazyEditor";
3031
import { IconButton } from "./core/IconButton";
3132
import type { Pair } from "./core/PairEditor";
@@ -130,16 +131,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
130131
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
131132

132133
const autocomplete: GenericCompletionConfig = useMemo(
133-
() => ({
134-
minMatch: 3,
135-
options:
136-
autocompleteUrls.length > 0
137-
? autocompleteUrls
138-
: [
139-
{ label: "http://", type: "constant" },
140-
{ label: "https://", type: "constant" },
141-
],
142-
}),
134+
() => getUrlCompletionConfig(autocompleteUrls),
143135
[autocompleteUrls],
144136
);
145137

apps/yaak-client/components/core/Editor/genericCompletion.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import type { CompletionContext } from "@codemirror/autocomplete";
1+
import type { Completion, CompletionContext } from "@codemirror/autocomplete";
22
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
33
import { defaultBoost } from "./twig/completion";
44

5+
export type GenericCompletion = GenericCompletionOption & {
6+
apply?: Completion["apply"];
7+
};
8+
59
export interface GenericCompletionConfig {
610
minMatch?: number;
7-
options: GenericCompletionOption[];
11+
options: GenericCompletion[];
812
}
913

1014
/**
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { Completion } from "@codemirror/autocomplete";
2+
import { EditorState, type TransactionSpec } from "@codemirror/state";
3+
import type { EditorView } from "@codemirror/view";
4+
import { describe, expect, test } from "vite-plus/test";
5+
import { applyUrlCompletion, getUrlCompletionConfig } from "./completion";
6+
7+
describe("applyUrlCompletion", () => {
8+
test("consumes an existing protocol suffix and preserves the rest of the URL", () => {
9+
expect(applyCompletion("http://rickandmortyapi.com/api/character", "http://", 4)).toBe(
10+
"http://rickandmortyapi.com/api/character",
11+
);
12+
});
13+
14+
test("inserts a protocol when there is no existing suffix", () => {
15+
expect(applyCompletion("htt", "http://", 3)).toBe("http://");
16+
});
17+
18+
test("replaces the full URL when accepting a saved URL", () => {
19+
expect(applyCompletion("htt://old.example/path", "https://new.example/api", 3)).toBe(
20+
"https://new.example/api",
21+
);
22+
});
23+
});
24+
25+
describe("getUrlCompletionConfig", () => {
26+
test("always includes protocols alongside saved URL options", () => {
27+
const config = getUrlCompletionConfig([{ label: "https://example.com" }]);
28+
29+
expect(config.options.map((option) => option.label)).toEqual([
30+
"http://",
31+
"https://",
32+
"https://example.com",
33+
]);
34+
expect(config.options.every((option) => option.apply === applyUrlCompletion)).toBe(true);
35+
});
36+
});
37+
38+
function applyCompletion(document: string, label: string, cursor: number) {
39+
let state = EditorState.create({ doc: document, selection: { anchor: cursor } });
40+
const view = {
41+
state,
42+
dispatch: (spec: TransactionSpec) => {
43+
state = state.update(spec).state;
44+
},
45+
} as unknown as EditorView;
46+
47+
applyUrlCompletion(view, { label } satisfies Completion, 0, cursor);
48+
return state.doc.toString();
49+
}
Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,51 @@
1-
import { genericCompletion } from "../genericCompletion";
2-
3-
export const completions = genericCompletion({
4-
options: [
5-
{ label: "http://", type: "constant" },
6-
{ label: "https://", type: "constant" },
7-
],
8-
minMatch: 1,
9-
});
1+
import { insertCompletionText, pickedCompletion, type Completion } from "@codemirror/autocomplete";
2+
import type { EditorView } from "@codemirror/view";
3+
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
4+
import {
5+
genericCompletion,
6+
type GenericCompletion,
7+
type GenericCompletionConfig,
8+
} from "../genericCompletion";
9+
10+
const protocolOptions: GenericCompletionOption[] = [
11+
{ label: "http://", type: "constant" },
12+
{ label: "https://", type: "constant" },
13+
];
14+
15+
export function getUrlCompletionConfig(
16+
options: GenericCompletionOption[],
17+
minMatch = 3,
18+
): GenericCompletionConfig {
19+
const urlOptions = [
20+
...protocolOptions,
21+
...options.filter(
22+
(option) => !protocolOptions.some((protocol) => protocol.label === option.label),
23+
),
24+
];
25+
return {
26+
minMatch,
27+
options: urlOptions.map<GenericCompletion>((option) => ({
28+
...option,
29+
apply: applyUrlCompletion,
30+
})),
31+
};
32+
}
33+
34+
export function applyUrlCompletion(
35+
view: EditorView,
36+
completion: Completion,
37+
from: number,
38+
to: number,
39+
) {
40+
const isProtocol = /^https?:\/\/$/.test(completion.label);
41+
const replaceTo = isProtocol
42+
? to + (view.state.sliceDoc(to, to + 3) === "://" ? 3 : 0)
43+
: view.state.doc.length;
44+
45+
view.dispatch({
46+
...insertCompletionText(view.state, completion.label, from, replaceTo),
47+
annotations: pickedCompletion.of(completion),
48+
});
49+
}
50+
51+
export const completions = genericCompletion(getUrlCompletionConfig([], 1));

0 commit comments

Comments
 (0)