🤔

[Obsidian] で Kiil and Yank をクリップボードを使って実装した

2023/03/14に公開

なぜかObsidianでは、^K^Yによる"Kill"&"Yank"に対応していません。正確に言うと^Kで"Kill"は動くのですが、消すだけです。ペーストできないんですね。

そこでいのうえたくや氏のobsidian-kill-and-yankを基にして、kill & yank機能を拡充して、IMEの状態チェックとクリップボードへの対応しました。
いのうえたくや氏に感謝。

[Obsidian] で kill line と yank したいでお話した通りですが、リファクタリングもすんで、いったん良い状況になりましたので、こちらで紹介と説明してpull requestしたい流れです。

現時点での完成したソースコードはこちらです。更新分について説明します。

main.ts
import { Editor, EditorPosition, MarkdownView, Plugin } from 'obsidian'
import { EditorView } from '@codemirror/view'

export default class KillAndYankPlugin extends Plugin {
  private editor: Editor
  private killRing: string
  private mark: EditorPosition | null = null

  private isComposing(view: MarkdownView): boolean {
    // @ts-expect-error
    const editorView = view.editor.cm as EditorView
    // console.log(`composing = ${editorView.composing}`);
    return editorView.composing
  }

  private isMark(editor: Editor): boolean {
    if (this.mark) {
      editor.setSelection(this.mark, editor.getCursor())
      return true
    }
    return false
  }

  async onload() {
    this.addCommand({
      id: 'kill-line',
      name: 'Kill line (Cut from the cursor position to the end of the line)',
      hotkeys: [{ modifiers: ['Ctrl'], key: 'k' }],
      editorCallback: (editor: Editor, view: MarkdownView) => {
        if (this.isComposing(view)) return

        const position: EditorPosition = editor.getCursor()
        const line: string = editor.getLine(position.line)

        const textToBeRetained = line.slice(0, position.ch)
        const textToBeCut = line.slice(position.ch)

        // this.killRing = textToBeCut
        navigator.clipboard.writeText(textToBeCut)

        editor.setLine(position.line, textToBeRetained)
        editor.setCursor(position, position.ch)
      },
    })

    this.addCommand({
      id: 'kill-region',
      name: 'Kill region (Cut the selection)',
      hotkeys: [{ modifiers: ['Ctrl'], key: 'w' }],
      editorCallback: (editor: Editor, view: MarkdownView) => {
        if (this.isComposing(view)) return
        this.mark = this.isMark(editor) ? null : null
        // this.killRing = editor.getSelection()
        navigator.clipboard.writeText(editor.getSelection())
        editor.replaceSelection('')
      },
    })

    this.addCommand({
      id: 'yank',
      name: 'Yank (Paste)',
      hotkeys: [{ modifiers: ['Ctrl'], key: 'y' }],
      editorCallback: (editor: Editor, view: MarkdownView) => {
        if (this.isComposing(view)) return

        navigator.clipboard.readText().then((text) => {
          editor.replaceSelection(text)
        })
        // editor.replaceSelection(this.killRing)
      },
    })

    this.addCommand({
      id: 'set-mark',
      name: 'Set mark (Toggle the start position of the selection)',
      hotkeys: [{ modifiers: ['Ctrl'], key: ' ' }],
      editorCallback: (editor: Editor, view: MarkdownView) => {
        this.mark = this.isMark(editor) ? null : editor.getCursor()
      },
    })
  }

  onunload() {}
}

IME 対応

たまたま、^K^Yに日本語入力時のキーカスタマイズがかぶっていたため、Kill-and-yankにてIMEの状態がONの時には実行せず、という機能を追加しました。そのチェック部分がこちらのisComposingです。IMEがON(True)/OFF(False)の状態をチェックして、その結果をBOOLEANで返却するだけです。

import { EditorView } from '@codemirror/view'
      :
      :
  private isComposing(view: MarkdownView): boolean {
    // @ts-expect-error
    const editorView = view.editor.cm as EditorView
    // console.log(`composing = ${editorView.composing}`);
    return editorView.composing
  }

Obsidian標準のライブラリには入っていなかったので、基のエディタ側であるCodeMirrorのライブラリをimportして利用するようになっています。

※ 注意事項と使い方 : Communicating with editor extensions

後は、editorCallback の直後にif (this.isComposing(view)) returnを入れておけば、IMEがOFFの時にしか作動しなくなるというだけの仕組みです。

      editorCallback: (editor: Editor, view: MarkdownView) => {
        if (this.isComposing(view)) return

クリップボード対応

基のソースコードですと、別のワークエリアを準備して^K^Yをクリップボードとは別の独立したエリアで実現していました。ただ、個人的に^Yでクリップボードの内容をペーストしたい気持ちがありました。ので、クリップボードへ対応しました。

ここは標準のクリップボード機能を利用します。^K^Wで、いわゆるコピーをする場合にはnavigator.clipboard.writeText()を利用します。ここで、コピーしたい内容が含まれているテキストを、関数の引数にしてあげるだけです。カンタン。

navigator.clipboard.writeText(editor.getSelection())

^Yでペーストするときは、このクリップボードの情報をnavigator.clipboard.readText()で読み出して、画面へ出力してあげることになります。このnavigator.clipboard.readText()は、Promiseで値が返却されるので、awaitさせるか、今回のようにPromiseっぽく処理してあげるとよくなります。

        navigator.clipboard.readText().then((text) => {
          editor.replaceSelection(text)
        })

今後の予定

ふと気がついたけど、^WだけじゃあれなのでM-wもあったら良さそうな気がしますね。なるほど。

Discussion