mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-17 10:11:28 +02:00
Fix URL autocomplete replacement (#510)
This commit is contained in:
@@ -39,6 +39,7 @@ import { BinaryFileEditor } from "./BinaryFileEditor";
|
|||||||
import { ConfirmLargeRequestBody } from "./ConfirmLargeRequestBody";
|
import { ConfirmLargeRequestBody } from "./ConfirmLargeRequestBody";
|
||||||
import { CountBadge } from "./core/CountBadge";
|
import { CountBadge } from "./core/CountBadge";
|
||||||
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
|
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
|
||||||
|
import { getUrlCompletionConfig } from "./core/Editor/url/completion";
|
||||||
import { Editor } from "./core/Editor/LazyEditor";
|
import { Editor } from "./core/Editor/LazyEditor";
|
||||||
import { InlineCode } from "@yaakapp-internal/ui";
|
import { InlineCode } from "@yaakapp-internal/ui";
|
||||||
import type { Pair } from "./core/PairEditor";
|
import type { Pair } from "./core/PairEditor";
|
||||||
@@ -285,16 +286,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
|
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
|
||||||
|
|
||||||
const autocomplete: GenericCompletionConfig = useMemo(
|
const autocomplete: GenericCompletionConfig = useMemo(
|
||||||
() => ({
|
() => getUrlCompletionConfig(autocompleteUrls),
|
||||||
minMatch: 3,
|
|
||||||
options:
|
|
||||||
autocompleteUrls.length > 0
|
|
||||||
? autocompleteUrls
|
|
||||||
: [
|
|
||||||
{ label: "http://", type: "constant" },
|
|
||||||
{ label: "https://", type: "constant" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[autocompleteUrls],
|
[autocompleteUrls],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { prepareImportQuerystring } from "../lib/prepareImportQuerystring";
|
|||||||
import { resolvedModelName } from "../lib/resolvedModelName";
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
||||||
import { CountBadge } from "./core/CountBadge";
|
import { CountBadge } from "./core/CountBadge";
|
||||||
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
|
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
|
||||||
|
import { getUrlCompletionConfig } from "./core/Editor/url/completion";
|
||||||
import { Editor } from "./core/Editor/LazyEditor";
|
import { Editor } from "./core/Editor/LazyEditor";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import type { Pair } from "./core/PairEditor";
|
import type { Pair } from "./core/PairEditor";
|
||||||
@@ -130,16 +131,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
|
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
|
||||||
|
|
||||||
const autocomplete: GenericCompletionConfig = useMemo(
|
const autocomplete: GenericCompletionConfig = useMemo(
|
||||||
() => ({
|
() => getUrlCompletionConfig(autocompleteUrls),
|
||||||
minMatch: 3,
|
|
||||||
options:
|
|
||||||
autocompleteUrls.length > 0
|
|
||||||
? autocompleteUrls
|
|
||||||
: [
|
|
||||||
{ label: "http://", type: "constant" },
|
|
||||||
{ label: "https://", type: "constant" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[autocompleteUrls],
|
[autocompleteUrls],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import type { CompletionContext } from "@codemirror/autocomplete";
|
import type { Completion, CompletionContext } from "@codemirror/autocomplete";
|
||||||
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
||||||
import { defaultBoost } from "./twig/completion";
|
import { defaultBoost } from "./twig/completion";
|
||||||
|
|
||||||
|
export type GenericCompletion = GenericCompletionOption & {
|
||||||
|
apply?: Completion["apply"];
|
||||||
|
};
|
||||||
|
|
||||||
export interface GenericCompletionConfig {
|
export interface GenericCompletionConfig {
|
||||||
minMatch?: number;
|
minMatch?: number;
|
||||||
options: GenericCompletionOption[];
|
options: GenericCompletion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type { Completion } from "@codemirror/autocomplete";
|
||||||
|
import { EditorState, type TransactionSpec } from "@codemirror/state";
|
||||||
|
import type { EditorView } from "@codemirror/view";
|
||||||
|
import { describe, expect, test } from "vite-plus/test";
|
||||||
|
import { applyUrlCompletion, getUrlCompletionConfig } from "./completion";
|
||||||
|
|
||||||
|
describe("applyUrlCompletion", () => {
|
||||||
|
test("consumes an existing protocol suffix and preserves the rest of the URL", () => {
|
||||||
|
expect(applyCompletion("http://rickandmortyapi.com/api/character", "http://", 4)).toBe(
|
||||||
|
"http://rickandmortyapi.com/api/character",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("inserts a protocol when there is no existing suffix", () => {
|
||||||
|
expect(applyCompletion("htt", "http://", 3)).toBe("http://");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("replaces the full URL when accepting a saved URL", () => {
|
||||||
|
expect(applyCompletion("htt://old.example/path", "https://new.example/api", 3)).toBe(
|
||||||
|
"https://new.example/api",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getUrlCompletionConfig", () => {
|
||||||
|
test("always includes protocols alongside saved URL options", () => {
|
||||||
|
const config = getUrlCompletionConfig([{ label: "https://example.com" }]);
|
||||||
|
|
||||||
|
expect(config.options.map((option) => option.label)).toEqual([
|
||||||
|
"http://",
|
||||||
|
"https://",
|
||||||
|
"https://example.com",
|
||||||
|
]);
|
||||||
|
expect(config.options.every((option) => option.apply === applyUrlCompletion)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function applyCompletion(document: string, label: string, cursor: number) {
|
||||||
|
let state = EditorState.create({ doc: document, selection: { anchor: cursor } });
|
||||||
|
const view = {
|
||||||
|
state,
|
||||||
|
dispatch: (spec: TransactionSpec) => {
|
||||||
|
state = state.update(spec).state;
|
||||||
|
},
|
||||||
|
} as unknown as EditorView;
|
||||||
|
|
||||||
|
applyUrlCompletion(view, { label } satisfies Completion, 0, cursor);
|
||||||
|
return state.doc.toString();
|
||||||
|
}
|
||||||
@@ -1,9 +1,51 @@
|
|||||||
import { genericCompletion } from "../genericCompletion";
|
import { insertCompletionText, pickedCompletion, type Completion } from "@codemirror/autocomplete";
|
||||||
|
import type { EditorView } from "@codemirror/view";
|
||||||
|
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
||||||
|
import {
|
||||||
|
genericCompletion,
|
||||||
|
type GenericCompletion,
|
||||||
|
type GenericCompletionConfig,
|
||||||
|
} from "../genericCompletion";
|
||||||
|
|
||||||
export const completions = genericCompletion({
|
const protocolOptions: GenericCompletionOption[] = [
|
||||||
options: [
|
|
||||||
{ label: "http://", type: "constant" },
|
{ label: "http://", type: "constant" },
|
||||||
{ label: "https://", type: "constant" },
|
{ label: "https://", type: "constant" },
|
||||||
],
|
];
|
||||||
minMatch: 1,
|
|
||||||
});
|
export function getUrlCompletionConfig(
|
||||||
|
options: GenericCompletionOption[],
|
||||||
|
minMatch = 3,
|
||||||
|
): GenericCompletionConfig {
|
||||||
|
const urlOptions = [
|
||||||
|
...protocolOptions,
|
||||||
|
...options.filter(
|
||||||
|
(option) => !protocolOptions.some((protocol) => protocol.label === option.label),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
minMatch,
|
||||||
|
options: urlOptions.map<GenericCompletion>((option) => ({
|
||||||
|
...option,
|
||||||
|
apply: applyUrlCompletion,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyUrlCompletion(
|
||||||
|
view: EditorView,
|
||||||
|
completion: Completion,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
) {
|
||||||
|
const isProtocol = /^https?:\/\/$/.test(completion.label);
|
||||||
|
const replaceTo = isProtocol
|
||||||
|
? to + (view.state.sliceDoc(to, to + 3) === "://" ? 3 : 0)
|
||||||
|
: view.state.doc.length;
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
...insertCompletionText(view.state, completion.label, from, replaceTo),
|
||||||
|
annotations: pickedCompletion.of(completion),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const completions = genericCompletion(getUrlCompletionConfig([], 1));
|
||||||
|
|||||||
Reference in New Issue
Block a user