Skip to content

선택 범위 동작

글을 고르고 호스트가 정한 동작을 그 범위에 적용하는 확장점이다(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 가 매우 길다. 서버에서 ![…](data:…)![…](image) 로 줄여 보낸다.
  • 스트리밍은 지원하지 않는다. run 이 끝나야 결과가 들어간다.

예: 정해진 문구로 바꾸기

모델 없이도 쓸 데가 있다.

js
{
  id: "callout",
  label: "주의 상자로",
  run: async ({ markdown }) => `> **주의**\n>\n> ${markdown.replace(/\n/g, "\n> ")}`,
}

제한

  • 대상은 최상위 블록 단위다. 문단 안의 한 구절만 바꾸는 동작은 만들 수 없다(그건 마크·서식의 영역이다).
  • 코드 블록 안에서는 말풍선도 슬래시 메뉴도 뜨지 않는다. 코드 블록을 대상으로 하려면 그 앞뒤 블록까지 걸쳐 고른다.
  • 마크다운 모드(mode="source")에서는 쓸 수 없다.