VScode の選択部分を Notion の codeblock として挿入 : Notion 解説(37)

はじめに

今朝、Notion dev の Slack に以下の記事の紹介が出ていました。 publish.obsidian.md

VSCode の選択部分から Notion の新しいページを作成し、codeblock として登録するものでした。Github で最終的なソースコードも紹介されています。 github.com

確かにコピーして、Notion 開いて貼り付けるよりも簡単ですね。ただ、新しいページが作られるよりも、私の場合には特定のページに追加されたほうがうれしい気がします。私の場合だとその日の雑務・振り返りのページに追加できるといいなということで、自分用に機能拡張を作ってみようと思いました。VScode の機能拡張を作るのも初めてなので勉強しながら試してみたいと思います。

VScode の機能拡張を作る

下準備

とりあえず作り方を調べてみます。最初に引っかかったのでこれを参考にしてみます。 techblog.gmo-ap.jp

機能拡張用のジェネレータが用意されているようなので、yarn で準備します。

yarn add --dev yo generator-code

yarn yo codeで雛形を作成してみます。今回の機能拡張の名前は「code-block-to-notion」にしてみました。実行結果を以下に示します。最初に何で記述するかの選択肢が出てきました。参考にしているものも TypeScript で作成しているので、ここでも TypeScript を選んでみます。その他必要項目を記述すると、最後に VScode で開くかと聞いてくれます。優しいね。

yarn run v1.22.17
warning package.json: No license field
$ /Users/hkob/node_modules/.bin/yo code

     _-----_     ╭──────────────────────────╮
    |       |    │   Welcome to the Visual  │
    |--(o)--|    │   Studio Code Extension  │
   `---------´   │        generator!        │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? code-block-to-notion
? What's the identifier of your extension? code-block-to-notion
? What's the description of your extension? Add code block to a Notion page
? Initialize a git repository? Yes
? Bundle the source code with webpack? Yes
? Which package manager to use? yarn

Writing in /Users/hkob/code-block-to-notion...
   create code-block-to-notion/.vscode/extensions.json
   create code-block-to-notion/.vscode/launch.json
   create code-block-to-notion/.vscode/settings.json
   create code-block-to-notion/.vscode/tasks.json
   create code-block-to-notion/package.json
   create code-block-to-notion/tsconfig.json
   create code-block-to-notion/.vscodeignore
   create code-block-to-notion/webpack.config.js
   create code-block-to-notion/vsc-extension-quickstart.md
   create code-block-to-notion/.gitignore
   create code-block-to-notion/README.md
   create code-block-to-notion/CHANGELOG.md
   create code-block-to-notion/src/extension.ts
   create code-block-to-notion/src/test/runTest.ts
   create code-block-to-notion/src/test/suite/extension.test.ts
   create code-block-to-notion/src/test/suite/index.ts
   create code-block-to-notion/.eslintrc.json
   create code-block-to-notion/.yarnrc

Changes to package.json were detected.

Running yarn install for you to install the required dependencies.
warning package.json: No license field
warning ../package.json: No license field
info No lockfile found.
warning code-block-to-notion@0.0.1: No license field
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.

Your extension code-block-to-notion has been created!

To start editing with Visual Studio Code, use the following commands:

     cd code-block-to-notion
     code .

Open vsc-extension-quickstart.md inside the new extension for further instructions
on how to modify, test and publish your extension.

To run the extension you need to install the recommended extension 'amodio.tsl-problem-matcher'.

For more information, also visit http://code.visualstudio.com and follow us @code.


? Do you want to open the new folder with Visual Studio Code? Open with `code`
✨  Done in 54.77s.

オリジナルを見てみると、「@notionhq/client」を使っているようなので、yarn で追加しておきます。

yarn add @notionhq/client

ついでに package.json に licenserepository を追加しておきました。また、README.md は設定の説明だけ追記しておきます。

# code-block-to-notion README

Right click a code block in vsc and create a notion block to a selected page.

## Extension Settings

* `code-block-to-notion.notionToken`: notion Token
* `code-block-to-notion.databaseId`: Task database ID
* `code-block-to-notion.titleTaskName`: Property title for task name
* `code-block-to-notion.titleTaskDate`: Property title for task date
* `code-block-to-notion.pageTitle`: Page title for a selected page
* `code-block-to-notion.openByApp`: Set true if you want open the page by Notion.app


## Release Notes

### 1.0.0

Initial release of code-block-to-notion

記述したコード

とにかく、オリジナルを真似て書いてみます。変更する点は以下の通りです。

  • オリジナルはページ名を入力して作成するが、私のは今日の「雑務・振り返りページ」に追記する
  • そのためページ作成ではなく、ブロックの追加になる
  • Notion ページをブラウザではなく、アプリでも開きたいのでフラグを設定

これらのために、README.md に書いたように 6 つの設定があります。()内は私の場合の設定値になります。

  • code-block-to-notion.notionToken: Notion のトークン (secret で始まる文字列)
  • code-block-to-notion.databaseId: 検索するデータベース ID (16進数の羅列)
  • code-block-to-notion.titleTaskName: データベースの名前のプロパティ名 (タスク名)
  • code-block-to-notion.titleTaskDate: データベースの日付のプロパティ名 (日付)
  • code-block-to-notion.pageTitle: 検索するページタイトル (雑務・振り返り)
  • code-block-to-notion.openByApp: ページを Notion で開く時には true (true)

あとは、だいたいいつものようなコードになります。今回は、Notion SDK を使っています。FILE_TYPES も自分がよく使うものを追加していました。それ以外の説明は省略します。コード自体は後述するパッケージ化する前に、VScode 上でデバッグが可能でした。流石にうまくシステム化されていますね。

// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
const { Client } = require("@notionhq/client");

const notion = new Client({
    auth: vscode.workspace.getConfiguration().get('code-block-to-notion.notionToken'),
});
const databaseId = vscode.workspace.getConfiguration().get('code-block-to-notion.databaseId');
const titleTaskName = vscode.workspace.getConfiguration().get('code-block-to-notion.titleTaskName');
const titleTaskDate = vscode.workspace.getConfiguration().get('code-block-to-notion.titleTaskDate');
const pageTitle = vscode.workspace.getConfiguration().get('code-block-to-notion.pageTitle');
const openByApp = vscode.workspace.getConfiguration().get('code-block-to-notion.openByApp');

const FILETYPES:any = {
    "ts": "typescript",
    "tsx": "typescript",
    "js": "javascript",
    "py": "python",
    "html": "html",
    "haml": "haml",
    "rb": "ruby",
    "css": "css",
    "tex": "LaTeX",
    "sty": "LaTeX",
    "m": "MATLAB",
    "rs": "Rust",
    "swift": "Swift",
    "yml": "YAML",
    "sh": "Shell",
    "c": "C",
    "h": "C"
};

const appendCodeBlock = async (pageId: string, code: string, language: string) => {
    if (pageId === '') {
        vscode.window.showErrorMessage('Pleage create today page');
        return;
    }
    if (code === '') {
        vscode.window.showErrorMessage('Please enter some code');
        return;
    }
    if (language === '') {
        vscode.window.showErrorMessage('Please enter a language');
        return;
    }
    const response = await notion.blocks.children.append({
        block_id: pageId,
        children: [
            {
                "type": "code",
                "object": "block",
                "code": {
                    "text": [{
                        "type": "text",
                        "text": {
                            "content": code
                        }
                    }],
                    "language": language
                }
            }
        ],
    });
    return response;
};

async function getTodayPage() {
    let myPage;
    try {
        var d = new Date();
        d.setHours(0);
        d.setMinutes(0);
        d.setSeconds(0);
        const lists = await notion.databases.query({
            database_id: databaseId,
            filter: {
                and: [
                    {
                        property: titleTaskName,
                        text: {
                            starts_with: pageTitle,
                        },
                    },
                    {
                        property: titleTaskDate,
                        date: {
                            after: d.toISOString(),
                        },
                    },
                ],
            },
        });
        myPage = lists.results[0];
    } catch (error) {
        return undefined;
    }
    return myPage;
}

const logic = async (editor: vscode.TextEditor | undefined) => {
    try {
        let codeBlock: string | undefined = editor?.document.getText(editor.selection);
        const filename: string[] | undefined = editor?.document.fileName.split('.');
        const fileType: string | undefined = filename?.slice(-1)[0];

        if (codeBlock === undefined) {
            vscode.window.showErrorMessage('Plaease select some code');
            return;
        }
        if (fileType === undefined) {
            vscode.window.showErrorMessage('Please select a file');
            return;
        }
        const language = FILETYPES[fileType];
        if (language === undefined) {
            vscode.window.showErrorMessage('Please select a valid file');
            return;
        }
        const myPage = await getTodayPage();
        if (myPage === undefined) {
            return;
        }
        const pageId = myPage.id;
        await appendCodeBlock(pageId, codeBlock, language);
        return myPage.url;
    } catch (err) {
        console.error(err);
        return null;
    }
};

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {

    // Use the console to output diagnostic information (console.log) and errors (console.error)
    // This line of code will only be executed once when your extension is activated
    console.log('Congratulations, your extension "code-block-to-notion" is now active!');

    // The command has been defined in the package.json file
    // Now provide the implementation of the command with registerCommand
    // The commandId parameter must match the command field in package.json
    let disposable = vscode.commands.registerCommand('code-block-to-notion.toNotion', async () => {
        const editor = vscode.window.activeTextEditor;
        const url = await logic(editor);
        if (url) {
            vscode.window.showInformationMessage(`Append code block successfully`, 'open page').then(async (value) => {
                if (value === 'open page') {
                    if (openByApp) {
                        vscode.env.openExternal(vscode.Uri.parse(url.replace('https', 'notion')));
                    } else {
                        vscode.env.openExternal(vscode.Uri.parse(url));
                    }
                }
            });
        } else {
            vscode.window.showErrorMessage('Could not create page -- check dev console');
        }
    });
    context.subscriptions.push(disposable);
}

// this method is called when your extension is deactivated
export function deactivate() {}

パッケージ化

うまく動くことが確認できたので、パッケージ化しました。 まず、パッケージのためのツールをインストールします。

 yarn add --dev vsce

これて vsce というコマンドが使えるので、以下のようにしてパッケージ化する。

yarn vsce package

パッケージが作成できたら、ローカルの VScode にインストールすることができます。

code --install-extension code-block-to-notion-0.0.1.vsix

f:id:hkob:20211117160241p:plain
インストールされたパッケージ

使用方法

コマンドの使い方はこんな感じになります。Notion に送りたい部分を選択した状態で、「Command」「Shift」「P」でコマンド入力ダイアログを出します。この中で「Add code block to a Notion page」を選びます。

f:id:hkob:20211117160550p:plain
コマンドを実行

成功すると右下に「open page」と書かれたダイアログが出てきます。

f:id:hkob:20211117160817p:plain
Notion を開くダイアログ

openByApp に true を設定しているので、Notion のアプリでページが開かれます。一番下にコードブロックが追加されていることがわかります。言語は保存されているファイル名により自動的に設定されます。ここではちゃんと「TypeScript」になっています。

f:id:hkob:20211117160936p:plain
追加された code block

おわりに

VScode の機能拡張を作ったのは初めてですが、それほどハマらずに作ることができました。これでも Alfred workflow などいろいろと作っていたからだと思います。今後また気になるものがあったら手を出してみようと思いました。

なお、作成したコードはこちらのリポジトリに置いておきましたので、もしよかったら参照してください。 github.com


hkob.notion.site