선택 범위 동작
글을 고르고 호스트가 정한 동작을 그 범위에 적용하는 확장점이다(0.10.17, src/plugins/selection-actions.ts). 편집기는 선택 범위를 마크다운으로 뽑아 호스트의 함수에 넘기고, 돌아온 마크다운으로 바꿔 넣을 뿐이다. 동작이 무엇인지는 모른다 — 번역, 맞춤법 검사, 정해진 문구로 바꾸기, AI 로 다듬기 어느 것이든 같은 통로를 탄다.
등록이 없으면(기본) 버튼도 없다.
어디에 뜨나
- 글을 고르면 뜨는 말풍선 툴바의 오른쪽 끝 — 등록한 동작마다 버튼 하나
- 슬래시 메뉴(
/) — 같은 동작이 항목으로 뜬다. 이때 대상은 커서가 있는 블록
등록
js
editor.selectionActions = [
{
id: "translate",
label: "영어로 번역",
run: async ({ markdown }) => await translate(markdown, "en"),
},
];React 는 selectionActions prop, Vue 도 같은 이름의 prop 이다. 배열을 새로 넣으면 버튼이 다시 그려진다.
ts
interface SelectionAction {
id: string;
label: string; // 버튼 툴팁 · 슬래시 메뉴 이름
description?: string; // 슬래시 메뉴 한 줄 설명
icon?: string; // SVG 마크업. 없으면 기본 아이콘
prompt?: true | { placeholder?: string; presets?: string[] }; // 입력칸 — 없으면 누르는 즉시 실행
run: (ctx: SelectionActionContext) => Promise<string | null>;
}호스트가 받는 것
ts
interface SelectionActionContext {
markdown: string; // 대상 범위 — 선택을 블록 경계로 넓힌 것
document: string; // 문서 전체
before: string; // 대상 앞의 글
after: string; // 대상 뒤의 글
input: string; // 입력칸에 쓴 글 (prompt 가 없으면 "")
}- 대상 범위는 선택을 최상위 블록 경계로 넓힌 것이다. 문단 한가운데 두 글자를 골라도 그 문단 전체가 대상이다. 목록이면 목록 전체, 표면 표 전체.
document·before·after를 같이 주는 이유: 대상만 보고 고치면 앞뒤와 어긋난 글이 나온다. 맥락이 필요한 동작(요약·보충·문체 통일)은 전문을 읽고 대상만 새로 쓰게 한다.
돌려주는 것
| 반환 | 편집기가 하는 일 |
|---|---|
| 마크다운 문자열 | 대상 범위를 그 글로 바꿔 넣는다. 덧붙이는 동작이면 원문 + 덧붙인 글을 함께 돌려준다 |
"" | 대상 범위를 지운다 |
null | 아무것도 하지 않는다 |
| 던짐 | 입력칸이 남고 이유(error.message)가 보인다. 고쳐서 다시 실행할 수 있다 |
- 결과는 한 번의 편집으로 들어간다. Ctrl/Cmd+Z 한 번이면 원래대로.
- 결과 범위가 선택된 채 남아 말풍선이 다시 뜬다 — 마음에 안 들면 바로 다른 동작을 돌린다.
- 처리하는 동안 대상 범위는 표시되고, 그 사이 문서를 고쳐도 범위는 따라간다.
입력칸
prompt 를 주면 버튼을 눌렀을 때 대상 아래에 입력칸이 뜬다. Enter 로 실행, Esc 로 취소. presets 는 자주 쓰는 지시를 칩으로 두고 누르면 그 글로 바로 실행한다.
js
{
id: "revise",
label: "다듬기",
prompt: { placeholder: "어떻게 고칠지 적으세요", presets: ["이유 추가", "더 자세히", "쉽게 풀어서", "짧게"] },
run: async (ctx) => { /* ctx.input 이 지시 */ },
}입력칸의 고정 문구(실행·취소·처리 중·실패)는 messages 로 바꾼다 — actionRun, actionCancel, actionWorking, actionFailed, actionInputPlaceholder.
예: AI 엔드포인트에 연결
언어 모델을 쓸 수 있는 서버가 있으면 이렇게 잇는다. 편집기는 모델을 모르므로 어떤 모델·어떤 지시문인지는 전부 서버 쪽이다.
js
editor.selectionActions = [
{
id: "ai",
label: "AI 로 다듬기",
prompt: { placeholder: "예: 그 이유를 한 문단 추가해줘", presets: ["이유 추가", "더 자세히", "쉽게 풀어서", "예제 추가", "짧게"] },
run: async ({ markdown, document, input }) => {
const res = await fetch("/api/ai/revise", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ document, selection: markdown, instruction: input }),
});
if (!res.ok) throw new Error(`서버 ${res.status}`); // 입력칸에 그대로 보인다
return (await res.json()).markdown; // 대상 범위의 새 판
},
},
];서버 쪽 지시문은 대개 이런 꼴이다.
아래는 문서 전체다. 그중 <selection> 부분만 사용자의 지시대로 새로 써서,
그 부분을 대신할 마크다운만 돌려줘라. 앞뒤 글과 문체·용어·수준을 맞춘다.
덧붙이라는 지시면 원문을 지키고 덧붙인다. 설명이나 코드 펜스로 감싸지 말고 마크다운 본문만.
<document>{document}</document>
<selection>{selection}</selection>
<instruction>{instruction}</instruction>- 모델이 대상 밖까지 고쳐 돌려주면 문서가 두 번 들어간다. "대상을 대신할 글만" 을 지시문에 못 박는다.
- 이미지가 data URL 로 박힌 문서는
document가 매우 길다. 서버에서를로 줄여 보낸다. - 스트리밍은 지원하지 않는다.
run이 끝나야 결과가 들어간다.
예: 정해진 문구로 바꾸기
모델 없이도 쓸 데가 있다.
js
{
id: "callout",
label: "주의 상자로",
run: async ({ markdown }) => `> **주의**\n>\n> ${markdown.replace(/\n/g, "\n> ")}`,
}제한
- 대상은 최상위 블록 단위다. 문단 안의 한 구절만 바꾸는 동작은 만들 수 없다(그건 마크·서식의 영역이다).
- 코드 블록 안에서는 말풍선도 슬래시 메뉴도 뜨지 않는다. 코드 블록을 대상으로 하려면 그 앞뒤 블록까지 걸쳐 고른다.
- 마크다운 모드(
mode="source")에서는 쓸 수 없다.