aimdevel’s blog

勉強したことを書きます

rust初心者がmoveではまった

rustでツールを作成する際にはまった箇所をメモしておく。
以下のドキュメントを参照しながら自分の理解をまとめる。

The Rust Programming Language 日本語版 - The Rust Programming Language 日本語版

何にはまったのか?

結論から言うと、値の所有権を理解していなかったためコンパイルに失敗した。
自分がコンパイルエラーを出したコードは以下のような構造になっていた。

struct Sample{
    name: String,
    num: u32,
}

fn main() {
    let mut samples: Vec<Sample> = Vec::new();
    for i in 1..4{
        samples.push(Sample{
            name: String::from("sample"),
            num: i,
        });
    }

    for sample in samples{
        println!("sample num:{}", sample.num);
    }

    println!("sample[2] name:{}, num:{}", samples[2].name, samples[2].num);

}

コンパイルエラーが以下。

   Compiling move-sample v0.1.0 (/root/git/move-sample)
error[E0382]: borrow of moved value: `samples`
  --> src/main.rs:19:43
   |
7  |     let mut samples: Vec<Sample> = Vec::new();
   |         ----------- move occurs because `samples` has type `Vec<Sample>`, which does not implement the `Copy` trait
...
15 |     for sample in samples{
   |                   ------- `samples` moved due to this implicit call to `.into_iter()`
...
19 |     println!("sample[2] name:{}, num:{}", samples[2].name, samples[2].num);
   |                                           ^^^^^^^ value borrowed here after move
   |
note: `into_iter` takes ownership of the receiver `self`, which moves `samples`
  --> /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/core/src/iter/traits/collect.rs:271:18
help: consider iterating over a slice of the `Vec<Sample>`'s content to avoid moving into the `for` loop
   |
15 |     for sample in &samples{
   |                   +

これはエラーメッセージの通り、「move occurs because samples has type Vec<Sample>, which does not implement the Copy trait」であり、解決手段は「for sample in &samples{」なのだが、これがどういうことなのか初心者なりに調べてみた。

値の所有権

rustの値には所有権という概念が存在する。ドキュメントによると以下のルールである。

所有権規則

  • Rustの各値は、所有者と呼ばれる変数と対応している。
  • いかなる時も所有者は一つである。
  • 所有者がスコープから外れたら、値は破棄される。

例えば、以下のように書くと値「1」の所有者は「num」ということになる。

let num:u32 = 1;

値のcopyとmove

以下のように書くと変数xの値が変数yにコピーされ、yとxどちらを参照しても同じ値が入っているというのは、どの言語でもほとんど共通の処理だと思う。

x = 1;
y = x;

上記はrustでも同様の意味になる。これをcopyという。
だが、rustでは同じ意味にならないことがある。それが以下の例だ。

let s1 = String::from("test");
let s2 = s1;

この場合は、s1の値はs2にコピーされない。所有者s1が持っていた値は所有者s2のものとなる。これをmoveという。
値の所有者は一つというルールがあるので、この処理の後にs1は値を所有していない。そのため、これ以降の処理でs1を参照しようとするとコンパイルエラーになる。
また所有者の変更は代入のように記述した場合だけではない。関数に値を渡したときやforループでinto_iter()で値を渡した時にも所有者の変更が行われる。

moveかcopyか

=で値のやり取りをする場合でも数値を扱う場合はcopyで、文字列を扱う場合はmoveと、意味合いが異なっているが、それは扱う値の型に依存している。ドキュメント曰く以下の型はcopyを使用できる。

  • あらゆる整数型。u32など。
  • 論理値型であるbool。trueとfalseという値がある。
  • あらゆる浮動小数点型、f64など。
  • 文字型であるchar。
  • タプル。ただ、Copyの型だけを含む場合。例えば、(i32, i32)はCopyだが、 (i32, String)は違う。

今回の失敗

今回のコンパイルエラーの原因は、copyできない値をforで処理しようとしたことだ。
以下のコードでは、暗黙的にinto_iter()が呼び出され、samplesの各要素の参照ではなく値を返す処理が行われてしまう。

for sample in samples{

そのため以下で参照しようと際に 値を所有していないのでコンパイルエラーになった。

println!("sample[2] name:{}, num:{}", samples[2].name, samples[2].num);

修正

これの修正はコンパイルエラーにも記述されているが、各要素の参照を返すように記述すればよい。

for sample in &samples{

まとめ

コンパイルエラーを通して、rustのmoveとcopyの概念を理解した()。

この記事を書いた後で気が付いたが、rustの所有権に関する記事はQiitaなどにいくつもあるようだ。
それだけ所有権の話はrustを使う上で重要という事なのだろう。

sdカードイメージのパーティションをマウントするツール作った

sdカードイメージファイルの特定のパーティションをマウントするツールを作成した。

作ろうと思った背景

ラズパイOSイメージなどのイメージファイル内のパーティションをマウントする方法は以下などがある。

これらでも問題なくマウントできるのだが、毎回losetupでloopデバイスを割り当てて使い終わったらloopデバイスを開放したり、offset位置を調べてmountコマンドを実行するのは億劫だった。
そこで、ツールを作成し何も考えずにイメージファイルのパーティションをマウントできるようにした。

ツール概要

ソースコードは以下。勉強も兼ねてrustで作成した。

https://github.com/aimdevel/part-mount

ビルド方法はgithubにあるREADME.mdに書いてあるので割愛する。

マウント方法

例としてラズパイのOSイメージファイルをマウントする。

OSイメージをダウンロードする。

$ wget https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz

xzを解凍する。

$ xz -d 2023-05-03-raspios-bullseye-armhf-lite.img.xz

マウントする。

パーティション1と2をそれぞれ、bootとrootfsという名前のディレクトリにマウントする。マウント操作なのでroot権限が必要。

$ mkdir boot && mkdir rootfs
$ sudo ./part-mount -p 1 2023-05-03-raspios-bullseye-armhf-lite.img boot
$ sudo ./part-mount -p 2 2023-05-03-raspios-bullseye-armhf-lite.img rootfs

結果を確認する。

マウントしたディレクトリを確認するとOSイメージの中身が見えていることを確認できる。

$ ls boot 
bcm2708-rpi-b.dtb       bcm2710-rpi-3-b-plus.dtb  cmdline.txt    fixup_x.dat       start4.elf
bcm2708-rpi-b-plus.dtb  bcm2710-rpi-cm3.dtb       config.txt     issue.txt         start4x.elf
bcm2708-rpi-b-rev1.dtb  bcm2710-rpi-zero-2.dtb    COPYING.linux  kernel7.img       start_cd.elf
bcm2708-rpi-cm.dtb      bcm2710-rpi-zero-2-w.dtb  fixup4cd.dat   kernel7l.img      start_db.elf
bcm2708-rpi-zero.dtb    bcm2711-rpi-400.dtb       fixup4.dat     kernel8.img       start.elf
bcm2708-rpi-zero-w.dtb  bcm2711-rpi-4-b.dtb       fixup4db.dat   kernel.img        start_x.elf
bcm2709-rpi-2-b.dtb     bcm2711-rpi-cm4.dtb       fixup4x.dat    LICENCE.broadcom
bcm2709-rpi-cm2.dtb     bcm2711-rpi-cm4-io.dtb    fixup_cd.dat   overlays
bcm2710-rpi-2-b.dtb     bcm2711-rpi-cm4s.dtb      fixup.dat      start4cd.elf
bcm2710-rpi-3-b.dtb     bootcode.bin              fixup_db.dat   start4db.elf
$ ls rootfs
bin   dev  home  lost+found  mnt  proc  run   srv  tmp  var
boot  etc  lib   media       opt  root  sbin  sys  usr

アンマウントする。

アンマウントは通常の方法でマウントした時と同じでumountコマンドを使用する。

2023/9/9 追記:この方法でアンマウントには成功するが、使用したloopbackデバイスはデタッチされないので、losetupコマンドでデタッチを行う必要がある。ツール自体を近いうちに修正予定。

$ sudo umount boot
$ sudo umount rootfs

まとめ

イメージファイル内の特定のパーティションをマウントするツールを作成した。
これでラズパイOSイメージファイルや、yoctoで作成したイメージファイルの中身を簡単に書き換えられるようになった(はず)。
また、勉強も兼ねてrustで作成したが、rust特有の仕様にはまってしまい少し苦戦した。ツールの構造自体はシンプルなのだが。。。
はまった部分については別途調べ直そうと思う。

vscodeのYocto変数hover機能作った

yoctoのレシピ作成に使用する変数のhover機能を、vscode拡張機能として作成した。
作ったものは以下。

GitHub - aimdevel/yocto-variable-hover: vscode extension that enables hover of yocto variables

背景・目的

yoctoのレシピを作成したり、設定を変更する際に、yoctoプロジェクトで定義されている変数を触る機会は多いと思う。
それら変数の説明は以下のページにまとまっている。
12 Variables Glossary — The Yocto Project ® 4.2.999 documentation

変数の使い方を知りたい場合はこのページ内で検索することになるが、いちいちページを開く、検索するという手間がかかって不便だ。
そこで、vscode上で変数の説明を見れるようにhover拡張機能を実装した。

使い方

ソースコードからビルドするか、以下のようにパッケージをインストールすると使用できる。
1. 以下のページから「yocto-variable-hover-0.0.2.vsix」をダウンロードする

Release prototype 2 · aimdevel/yocto-variable-hover · GitHub

  1. vscodeのサイドバーで「extensions」のタブに移動する。

  2. 「Views and More Actions...」(・・・のようなアイコン)から「Install from VSIX...」を選択し、先ほどダウンロードしたファイルを選択しインストールする。

  3. vscodeでyocto関連のファイルを開き、マウスカーソルを変数に乗せると、以下のように説明が表示される。

まとめ

yocto変数の説明を表示する機能を作成した。
これでエディタで説明を見れるようになったので、不便ではない、はず。
表示が小さくて見づらいところはあるが、それはほかの言語のhoverでも同じなので気にしないことにする。
次は、この拡張機能の内部構造の説明でもしようかと思う。

TypeScript+SequelizeでSQLiteを使う

概要

vscode拡張機能でデータベースを使いたくなったため、TypeScriptからデータベースを使えるSequelizeを試してみた。 使用したデータベースはSQLiteで、以下のページを参考に作業した。

Getting Started | Sequelize

インストール

$ apt update
$ apt install sqlite3
$ npm install --save sequelize
$ npm install --save sqlite3

TypeScriptでdb操作

データベース接続

手順の通りに作成する。

import { Model, DataTypes, Sequelize } from 'sequelize';

const sequelize = new Sequelize({
  dialect: 'sqlite',
  storage: '/path/to/database.sqlite'
});

try {
  sequelize.authenticate();
  console.log('Connection has been established successfully.');
} catch (error) {
  console.error('Unable to connect to the database:', error);
}

Modelの定義

データベースのテーブル構造を定義する。

// define Model
class User extends Model {
  declare id: number; // this is ok! The 'declare' keyword ensures this field will not be emitted by TypeScript.
  declare name: string;
}

User.init({
  id: {
    type: DataTypes.INTEGER,
    autoIncrement: true,
    primaryKey: true
  },
  name: {
    type: DataTypes.STRING
  }
}, { sequelize });

データを追加する。

Modelの定義で作成したUserを使用してデータを追加する。

// insert
(async () => {
  await User.sync();
  const user = await User.create({ name: "taro" });
  const user2 = await User.create({ name: "jiro" });
})();

データを取り出す

Userのデータをすべて取り出す。

// select
(async () => {
  await User.sync();
  const users = await User.findAll();
  users.map(tmp=> console.log(tmp.id + ":" + tmp.name));
})();

まとめ

Sequelizeを試した。
今回は単純なデータの格納と読み出しだけを行ったが、Sequelizeを使えばデータベース操作は簡単に行えるように感じた。
TypeScriptでデータベースが使用できるようになったので、目的は達成。

containerd+wasmを動かしてみた。

最近こんな記事を見つけたので、この機会にcontainerd+wasmを動かしてみた。

www.publickey1.jp

概要

以下のページの手順に従ってサンプルを実行を試す。
その後、rustのHello Worldを動かす。

GitHub - containerd/runwasi: Facilitates running Wasm / WASI workloads managed by containerd

確認環境

ubuntu22.04 on raspberrypi4
sdカードに新たにOSを書き直したものを使用した。

環境構築

ツール類のインストール

  • ubuntuにツールをインストールする。
$ sudo apt update
$ sudo apt install build-essential libclang-dev
  • containerdをインストールする。
    以下のページを参考に作業した。

containerd/getting-started.md at main · containerd/containerd · GitHub

$ wget https://github.com/containerd/containerd/releases/download/v1.6.18/containerd-1.6.18-linux-arm64.tar.gz
$ sudo tar Cxzvf /usr/local containerd-1.6.18-linux-arm64.tar.gz

containerdをsystemdに登録する。

$ wget https://raw.githubusercontent.com/containerd/containerd/main/containerd.service
$ sudo mkdir -p /usr/local/lib/systemd/system/
$ sudo cp containerd.service /usr/local/lib/systemd/system/containerd.service
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now containerd
  • rustのインストールが必要なので、以下のサイトを参考に作業した。

Installation - The Rust Programming Language

$ curl --proto '=https' --tlsv1.3 https://sh.rustup.rs -sSf | sh

途中で以下のように聞かれるので1で回答。

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation

最後に環境を有効化する。

$ source "$HOME/.cargo/env"

WasmEdgeのインストール

Wasmedgeをランタイムとして使用するために以下でインストールする。

$ curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash
$ sudo -E sh -c 'echo "$HOME/.wasmedge/lib" > /etc/ld.so.conf.d/libwasmedge.conf'
$ sudo ldconfig

runwasiのクローン

$ git clone https://github.com/containerd/runwasi.git

テスト実行

手順にあるのでテストを実行してみる。

$ cd runwasi
$ cargo test -- --nocapture

shimをビルド、インストールする

$ make build
$ sudo make install

デモの実行

$ rustup target add wasm32-wasi
$ make load
$ sudo ctr run --rm --runtime=io.containerd.wasmedge.v1 ghcr.io/containerd/runwasi/wasi-demo-app:latest testwasm /wasi-demo-app.wasm echo 'hello'

さらにアプリに渡すオプションを変更してもう一回実行する。

$ sudo ctr run --rm --runtime=io.containerd.wasmedge.v1 ghcr.io/containerd/runwasi/wasi-demo-app:latest testwasm /wasi-demo-app.wasm daemon

アプリの作成

デモアプリを参考にして、Hello Worldを実行してみる。
デモアプリはRustで書かれている。Rustは書いたことないけど、これを機に勉強していこうかな。

rustプロジェクトの作成

$ cd ~/
$ cargo new hello-containerd-wasm 
$ cd hello-containerd-wasm

デモアプリの資材をコピー

runwasi/crates/wasi-demo-appからbuild.rsファイルをコピーする。

$ cp /path/to/runwasi/crates/wasi-demo-app/build.rs ./

その後、このファイル内の"wasi-demo-app"という文字列を、"hello-containerd-wasm"に置換する。

Cargo.tomlを編集

wasi-demo-appのCargo.tomlを参考にして以下のように変更する。

[package]
name = "hello-containerd-wasm"
version = "0.1.0"
edition = "2021"

[build-dependencies]
tar = {version="0.4", optional=true}
sha256 = {version="1.1", optional=true}
log = {version="0.4", optional=true}
env_logger = {version="0.10", optional=true}
oci-spec = {version="0.6", optional=true}
oci-tar-builder = { git = "https://github.com/containerd/runwasi.git", optional=true }
anyhow = {version="1.0", optional=true}

[features]
default = []
oci-v1-tar = ["default", "tar", "sha256", "log", "env_logger", "oci-spec", "oci-tar-builder", "anyhow"]

アプリをwasmにビルド

$ cargo build --target wasm32-wasi

アプリをwasmedgeで動確

$ wasmedge target/wasm32-wasi/debug/hello-containerd-wasm.wasm

コンテナイメージ作成

$ cargo build --features oci-v1-tar

これで、"target/wasm32-wasi/debug/img.tar"が生成される。

containerdへ取り込み

$ sudo ctr -n default image import --all-platforms target/wasm32-wasi/debug/img.tar

以下のコマンドで、取り込めたことを確認。 "ghcr.io/containerd/runwasi/hello-containerd-wasm:latest"が今回取り込んだイメージ。

$ sudo ctr images ls
REF                                                     TYPE                                       DIGEST                                                                  SIZE    PLATFORMS LABELS
ghcr.io/containerd/runwasi/hello-containerd-wasm:latest application/vnd.oci.image.manifest.v1+json sha256:2d6614491b2a4def9f211a54171f1117e7d623f939d7a4aa26077abbf765128b 2.1 MiB wasi/wasm -
ghcr.io/containerd/runwasi/wasi-demo-app:latest         application/vnd.oci.image.manifest.v1+json sha256:fdff1828d29ccb564a0332cfb4a9cd7b1f982e1ecc56a1d3de3f26156e8da675 2.5 MiB wasi/wasm -

コンテナ実行

デモアプリと同様に以下のように実行できた。

$ sudo ctr run --rm --runtime=io.containerd.wasmedge.v1 ghcr.io/containerd/runwasi/hello-containerd-wasm:latest mytestwasm
/hello-containerd-wasm.wasm
Hello, world!

まとめ

containerdでwasmの実行を試した。
wasmコンテナを作成してそれをcontainerdに登録、実行できた。

yoctoのcve-checkを試してみる

前から存在だけ知っていたyoctoのcve-checkを実際に動かしてみる。

作業概要

yoctoのレイヤmeta-raspberrypiの最小構成でcve-checkを実施し、どのような結果が得られるか確認する。以下のページの「3.34 Checking for Vulnerabilities」 を参考に作業する。

https://docs.yoctoproject.org/dev-manual/common-tasks.html#checking-for-vulnerabilities

yoctoバージョンはkirkstoneで、ビルド環境はあらかじめ用意してあるものを使用する。
作業日は、2023/1/28。

作業手順

meta-raspberrypi用ソフトをダウンロードする。

$ mkdir yocto-cve-check-raspi
$ cd yocto-cve-check-raspi
$ git clone git://git.yoctoproject.org/poky -b kirkstone
$ git clone https://github.com/agherzan/meta-raspberrypi.git -b kirkstone

環境設定を行う

以下のコマンドで環境を初期化する。

$ . poky/oe-init-build-env rpi-build

rpi-build/conf/bblayers.confを編集してmeta-raspberrypiを追加する。

BBLAYERS ?= " \
  /home/user/yocto-cve-check-raspi/poky/meta \
  /home/user/yocto-cve-check-raspi/poky/meta-poky \
  /home/user/yocto-cve-check-raspi/poky/meta-yocto-bsp \
  /home/user/yocto-cve-check-raspi/meta-raspberrypi \
  "

rpi-build/conf/local.confを編集する。ターゲットとするボードを指定と、cve-checkの指定を行う。今回はラズパイ4の64bitを対象にする。

MACHINE = "raspberrypi4-64"
INHERIT += "cve-check"

ビルド

ビルドを実行する。

$ bitbake core-image-minimal

結果確認

結果はrpi-build/tmp/deploy/cve/に格納されている。 参考にしたページと同様にflex-nativeの結果を見ると以下のようになっている。

LAYER: meta
PACKAGE NAME: flex-native
PACKAGE VERSION: 2.6.4
CVE: CVE-2016-6354
CVE STATUS: Patched
CVE SUMMARY: Heap-based buffer overflow in the yy_get_next_buffer function in Flex before 2.6.1 might allow context-dependent attackers to cause a denial of service or possibly execute arbitrary code via vectors involving num_to_read.
CVSS v2 BASE SCORE: 7.5
CVSS v3 BASE SCORE: 9.8
VECTOR: NETWORK
MORE INFORMATION: https://nvd.nist.gov/vuln/detail/CVE-2016-6354

LAYER: meta
PACKAGE NAME: flex-native
PACKAGE VERSION: 2.6.4
CVE: CVE-2019-6293
CVE STATUS: Ignored
CVE SUMMARY: An issue was discovered in the function mark_beginning_as_normal in nfa.c in flex 2.6.4. There is a stack exhaustion problem caused by the mark_beginning_as_normal function making recursive calls to itself in certain scenarios involving lots of '*' characters. Remote attackers could leverage this vulnerability to cause a denial-of-service.
CVSS v2 BASE SCORE: 4.3
CVSS v3 BASE SCORE: 5.5
VECTOR: NETWORK
MORE INFORMATION: https://nvd.nist.gov/vuln/detail/CVE-2019-6293

statusは以下を意味するようだ。
- Patched
セキュリティ問題を修正するパッチが適用されている。
- Unpatched
パッチが当たっておらず、調査が必要。
- Ignored
分析の結果、この問題を無視すると判断したもの。

また、ビルドしたイメージ全体に含まれるcve情報はrpi-build/tmp/deploy/images/raspberrypi-64/core-image-minimal-raspberrypi4-64.cveとして格納されている。

cve-checkの情報元は?

ではこのcve情報はどこから来ているのか?もちろん自動でアップデートはされるだろうが出所くらいは知っておきたい。
そこでcve-check.bbclassファイルを調べてみる。

cve-check.bbclass

ファイル内を見たところ以下でcheck_cves関数でデータベースを読んでいる。

import sqlite3
db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro")
conn = sqlite3.connect(db_file, uri=True)

ここで使用されている CVE_CHECK_DB_FILE は同ファイル内で以下のように定義されている。

CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/nvdcve_1.1.db"

nvccve_1.1.dbがcve情報を格納したデータベースとなっていそうだ。 続けてこのファイルを取得する処理を探すと、cve-update-db-native.bbという、いかにもな名前のbbファイルを発見した。

cve-update-db-native.bb

このファイル内で以下の記述の通りデータベースのもととなるファイルを取得しているようだ。

NVDCVE_URL ?= "https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-"
# CVE database update interval, in seconds. By default: once a day (24*60*60).
# Use 0 to force the update
# Use a negative value to skip the update
CVE_DB_UPDATE_INTERVAL ?= "86400"

このNVDCVE_URLから以下のようにurlを組み立てデータをダウンロードしている。

            year_url = (d.getVar('NVDCVE_URL')) + str(year)
            meta_url = year_url + ".meta"
            json_url = year_url + ".json.gz"

試しに、 https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-2020.metahttps://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-2020.json.gz をダウンロードしてみた。
nvcve-1.1-2022.metaはメタデータで以下の内容だった。

lastModifiedDate:2023-01-28T03:00:17-05:00
size:97658996
zipSize:5301183
gzSize:5301047
sha256:2D59A166D4C1A9E80AFB8F9B24A23AC0C9332195922115B65C3A27FC5F853A1C

nvdcve-1.1-2020.json.gzはcve情報を含む巨大なjsonファイルだった。

cve-update-db-native.bbではこのjsonファイルからデータを取得して内部のデータベースをアップデートしているようだ。

まとめ

yoctoのcve-checkを試し、cveの一覧を取得できた。
情報元のnvdcveは1日1回の更新らしいので、脆弱性情報は毎日確認して使用してるパッケージについての情報は見落とさないように通知する仕組みが必要そうだ。

vscode拡張機能としてhoverを実装する

概要

vscode拡張機能にhoverを実装するために以下を行う。 今回はlanguage serverとして実装する。
* ベースとするソースコードの入手
* 初期化処理の編集
* hover関数を実装
* hover関数をサーバに登録

ベースとするソースコードの入手

以下のリポジトリをクローンして、lsp-sampleのソースコードを使用する。
クローン後にmoduleをインストールする。

github.com

$ git clone https://github.com/microsoft/vscode-extension-samples.git
$ cd vscode-extension-samples/lsp-sample
$ npm install

その後vscodeでこのvscode-extension-samples/lsp-sampleを開く。


以下ではserver/src/server.tsを編集していく。

importの追加

以下のようにHoverParamsを追加でimportする。

import {
    createConnection,
    TextDocuments,
    Diagnostic,
    DiagnosticSeverity,
    ProposedFeatures,
    InitializeParams,
    DidChangeConfigurationNotification,
    CompletionItem,
    CompletionItemKind,
    TextDocumentPositionParams,
    TextDocumentSyncKind,
    InitializeResult,
    HoverParams,    // ここを追加
    Hover   // ここを追加
} from 'vscode-languageserver/node';

初期化処理の編集

初期化時に設定するcapabilitiesにhoverの設定を追加する。server.tsの67行目あたりに追加すればよい。

// for hover
result.capabilities.hoverProvider = true;

hover用関数を実装

hover用の関数を実装する。 server.tsの適当な場所に以下を追記する。

// for hover
async function HoverSample(params:HoverParams): Promise<Hover> {
    var contents = "# Hover Test!\nyour hover!!!"
    var range = {
        start: { line: params.position.line, character: params.position.character },
        end : { line: params.position.line, character: params.position.character+5 }
    }
    return {
        contents,
        range,
    }
}

hover用関数をサーバに登録

実装した関数をサーバに登録する。server.tsの適当な場所に以下を追記する。

// for hover
connection.onHover(params => HoverSample(params));

以上でhoverの実装ができた。

F5で拡張機能を起動し、Plain Textファイルの適当なところにマウスを持っていくとhoverで文字列が表示されるはず。

Hoverレスポンスの構造

Hoverリクエストの結果として返すデータ構造を見ていく。
以下のサイトを参考にする。
Specification

Hoverは以下の構造になっている。

/**
 * The result of a hover request.
 */
export interface Hover {
    /**
    * The hover's content
    */
    contents: MarkedString | MarkedString[] | MarkupContent;

    /**
    * An optional range is a range inside a text document
    * that is used to visualize a hover, e.g. by changing the background color.
    */
    range?: Range;
}
  • contentsは以下の定義で、文字列で指定できる。
type MarkedString = string | { language: string; value: string };
  • rangeは以下の形式で、hoverの対象となる文字列がドキュメントのどの位置にあるかを示す。
interface Range {
    /**
    * The range's start position.
    */
    start: Position;

    /**
    * The range's end position.
    */
    end: Position;
}

interface Position {
    /**
    * Line position in a document (zero-based).
    */
    line: uinteger;

    /**
    * Character offset on a line in a document (zero-based). The meaning of this
    * offset is determined by the negotiated `PositionEncodingKind`.
    *
    * If the character value is greater than the line length it defaults back
    * to the line length.
    */
    character: uinteger;
}

今回はHoverで通知する情報はほぼ固定だったが、これらの値を変更することで必要な情報をhoverで通知できる。