LoginSignup
16
19

More than 5 years have passed since last update.

Redmine 4.0(Rails 5.2)に用語集(Glossary)プラグインを対応させる

Last updated at Posted at 2018-07-07

はじめに

Redmineは本日(2018-07-04)現在、バージョン3.4.6が最新で、これはRuby on Rails 4.2上で動作します。

一方、次のメジャーバージョンアップとなるRedmine 4.0に向けた開発が進められており、これはRuby on Rails 5.2上で動作することを目指しています。

Redmine 3.xで動作しているプラグインについて、Redmine 4.0(未だリリース前なので開発ブランチの最新を使用)で動かそうとすると、Redmineが稼働するフレームワークであるRuby on Railsのメジャーバージョンアップにおける非互換性に起因するエラーとなってしまうものが多数出る見込みです。

本記事は、Redmine 3.4で(表面的には)動作している用語集(Glossary)プラグインを、Redmine 4.0で動くように修正した作業の中で得た知見をまとめたものです。

非互換性とその対応

alias_method_chainの削除

alias_method_chainは、Rails(ActiveRecord)で提供される、既存のメソッドを差し替える、あるいは呼び出し前後に処理を挟むことができる機能です。プラグインではRedmine本体のコードを修正することなく処理を挟むことができるので広く使われています。これが、Ruby on Rails 5.1で削除されたため、alias_method_chainを使用しているプラグインは別な手段で処理を実現することが必要になりました。代替手段としてはModuleのprependを使用することが提案されています。

次は、用語集プラグインでalias_method_chainを使用している箇所をprependに置き換えたときの差分です。

alias_method_chainをrependに置き換え(Github)

prependに置き換えた方法を様式的に示すと次のとおりです。

  • 独自のモジュールPを定義
  • モジュールPの中に差し替え対象メソッドと同じシグニチャを定義し、alias_method_chain :xxx :yyy と指定していた場合、xxx_with_yyyの実装を丸ごと持ってくる
  • 実装の中にxxx_without_yyyの呼び出しがあれば、superに置換する
  • 差し替え対象メソッドを持つクラスまたはモジュールCとすると、C.prepend P を呼ぶ

マイグレーションクラスの継承元に素のMigrationは使えない

これまで、データベースのマイグレーション処理では、ActiveRecord::Migrationを継承していました。

001_create_terms.rb
class CreateTerms < ActiveRecord::Migration
  def self.up
  :

このコードがRails 5では、エラーとなります。

Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Rails release the migration was written for:

  class CreateTerms < ActiveRecord::Migration[4.2]

マイグレーションコードを記述したときのRailsバージョンを指定する必要があります。Migration Versioningと呼ばれる仕組みで、Railsのバージョンアップにより生成されるスキーマが変わることがあるため(NOT NULL制約の有無、INTかBIGINTか、など)、意図しないスキーマが生成されないようバージョンを指定するもののようです。

次は、用語集プラグインでマイグレーションのバージョン指定を追加したときの差分です。

Migrationにバージョン付け(Github)

バージョンは、Redmine 3.xが動作するRuby on Rails 4.2を指定しました。

非推奨だったbefore_filterの削除

before_filter で指定したメソッドは、コントローラーのアクションが実行される際、そのアクション実行の直前に実行されます。例えば、各アクションは認証されたユーザーに限り実行可能であるとする場合、各アクションのメソッドの中で認証チェックするのではなく、before_filterで認証チェックするメソッドを指定しておくといった使い方です。

アクションの前に実行するので、よりふさわしい命名であるbefore_actionに変更され、Rails 5.1でbefore_filterは削除されています。

置き換えは単純にbefore_filterをbefore_actionに修正するだけです。

-  before_filter :find_project, :authorize                                                      
-  before_filter :find_term, :only => [:show, :edit, :destroy]                                  
-  before_filter :retrieve_glossary_style, :only => [:index, :show, :show_all, :import_csv_exec]
+  before_action :find_project, :authorize                                                      
+  before_action :find_term, :only => [:show, :edit, :destroy]                                  
+  before_action :retrieve_glossary_style, :only => [:index, :show, :show_all, :import_csv_exec]

次は、用語集プラグインでbefore_filterをbefore_actionに置き換えたときの差分です。

before_filterをbefore_actionに置き換え(Github)

コントローラーのunloadable

コントローラークラスにおまじないのように記載されているunloadableですが、Redmine 3.xでは不要なようです。Redmine本体では次のチケットで削除されています。
http://www.redmine.org/issues/27963

そこで、今回バッサリ削除します。

開発モードで実行する際、unloadableが記載されていると、コントローラーのソースコードを変更してクライアントから再度アクセスすると「Unable to autoload constant XxxController ...」エラーとなってしまいます。

次は、用語集プラグインのコントローラからunloadableを削除したときの差分です。

unloadableをコントローラーから削除(Github)

モデルのunloadable

モデルにもunloadableの記載があったので削除します。

unloadableをモデルから削除(Github)

attr_accessibleの削除

モデルクラスにattr_accessibleで変更可能な属性を指定することで、書き換え可能な属性とするのに使います。

このattr_accessibleは、Rails 4の時点で非推奨となっており、Rails 5では削除されています。実は、用語集プラグインをRedmine 3.x対応させた際に非推奨なのを承知で持ち込んでしまいました。

置き換えは、「ストロングパラメーター」という仕組みを使います。Railsでは、HTTPリクエストとしてクライアントから送られてくるパラメーターをそのままモデルに渡してモデルの作成・更新を行うマスアサインメントという機構があります。これはコーディングが楽ですが、不用意に属性が更新され脆弱性をもたらすことになりかねません。ストロングパラメーターは、変更可能な属性を明示的に指定し、その属性以外がパラメーターに含まれていてもモデルには反映しないというものです。

GlossaryStylesモデルのattr_accessible削除とストロングパラメーター導入

まず、GrossaryStyleクラスからattr_accessibleを削除します。

-  attr_accessible :groupby

次に、GlossaryStylesControllerクラスにストロングパラメーターを実装します。

-        @glossary_style = GlossaryStyle.new(params[:glossary_style])
+        @glossary_style = GlossaryStyle.new(glossary_style_params)
  :
+  private
+
+  def glossary_style_params
+    params.require(:glossary_style).permit(:groupby)
+  end

同様に、Termクラスからattr_accessibleを削除し、TermsControllerにストロングパラメーターを実装します。

-  attr_accessible :project_id, :category_id, :author, :name, :name_en, :datatype, :codename, :description,
-                  :rubi, :abbr_whole
-    @term = Term.new(params[:term])
+    @term = Term.new(term_params)
     :
+  def term_params                                                                             
+    params.require(:term).permit(                                                            
+      :project_id, :category_id, :author, :name, :name_en, :datatype, :codename, :description,
+      :ruby, :abbr_whole                                                                      
+    )                                                                                         
+  end                                                                                         

さらに、TermCategoryクラスからattr_accessibleを削除し、TermCategoriesControllerクラスにストロングパラメーターを実装します。

次は、用語集プラグインでattr_accessibleを使っていた箇所を、ストロングパラメーターに置き換えたときの差分です。

削除されたattr_accessibleに替わるストロングパラメーターの実装(Github)

acts_as_listの削除

Redmine 3.xでは、Redmineディレクトリ>/lib/plugins/acts_as_list が存在していました。しかし、Redmine 4.xではこのacts_as_listが削除されています。

このacts_as_listはgemで存在しているので、別途インストールすることとします。用語集プラグインのディレクトリにGemfileを作成し、acts_as_listをgemでインストールします。

gem 'acts_as_list'

Redmineディレクトリでbundleを実行します。

パラメーターはハッシュを継承しなくなった

HTTPリクエストのパラメーターはRails 5以降はハッシュを継承しなくなったので、ハッシュを引数に取るメソッドにパラメーターを渡していたコードがエラーとなってしまいます。

unable to convert unpermitted parameters to hash

使用箇所の例は次となります。

  • app/views/glossary/index.html.erb
    <%= f.link_to 'CSV', :url => params %>

明示的にハッシュに変換し、かつpermitを有効にします。

-    <%= f.link_to 'CSV', :url => params %>             
+    <%= f.link_to 'CSV', :url => params.permit!.to_h %>

次は、用語集プラグインでリクエストパラメーターをハッシュに変換、permitを与えたときの差分です。

リクエストパラメーターをpermitして明示的にハッシュに変換(Github)

画像ファイルの削除

Redmine 3.xまでは、Redmineインストールディレクトリ/public/imagesディレクトリに次の画像ファイルが提供されていました。

  • 1downarrow.png
  • 1uparrow.png
  • 2downarrow.png
  • 2uparrow.png

しかし、Redmine最新ブランチ(Redmine 4.0向け開発)のpublic/imagesディレクトリにはこれらのファイルが含まれていません。

そこで、プラグインディレクトリのassets/imagesディレクトリにこの4つのファイルを置くことにします。

また、プラグインディレクトリにある画像ファイルを使用する際はプラグイン名の指定が必要となるので、リンクの画像ファイル指定にプラグイン名の指定を追加します。

-                <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
-                <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> -
-                <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
-                <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>
+                <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest), :plugin => 'redmine_glossary'), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
+                <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher), :plugin => 'redmine_glossary'), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> -
+                <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower), :plugin => 'redmine_glossary'), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
+                <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest), :plugin => 'redmine_glossary'), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>

次は、用語集プラグインに画像ファイルを追加しリンクで画像の指定にプラグイン名を追加したときの差分です。

Redmine本体から削除された画像ファイルをプラグインのassetsに追加し、画像リンクの指定にプラグイン名を追加(Github)

ActiveRecordのupdate_allの引数

ユニットテスト(モデル)を書いてテスト実行して見つけた非互換なコードにupdate_allの引数指定がありました。

Rails 4.0以降の引数指定

Rails 5.2のドキュメントでは、update_allのシグニチャは次です。

#update_all(updates) ⇒ Object
  • updates: 文字列、配列、またはハッシュでSQLの設定(SET部分)を表現する

使い方の例は次です。

User.where(name: nil).update_all(name: 'No name')

Rails 4.0以降でこのシグニチャに変更されています。

Rails 3.2の引数指定

Rails 3.2でのupdate_allの引数指定は次です。

update_all(updates, conditions = nil, options = {}) public
  • updates: 文字列、配列、またはハッシュでSQLの設定(SET部分)を表現する
  • conditions: 文字列、配列、またはハッシュでSQLの条件(WHERE部分)を表現する

一方、用語集プラグインでの使い方は次です。

app/models/term_category.rb

Term.update_all("category_id = #{reassign_to.id}", "category_id = #{id}")

これは、Rails 3.2でのupdate_allの指定となります。

修正

ActiveRecordのupdate_all呼び出し時の引数指定をRails 3.2版のシグニチャからRails 4.2以降(5.2を含む)のシグニチャに対応するよう変更(Github)

非推奨警告で今後削除されるもの

ルーティングに:actionは使わない

サーバーを起動する際、次の警告メッセージが表示されました。

DEPRECATION WARNING: Using a dynamic :action segment in a route is deprecated and will be removed in Rails 6.0. (called from instance_eval at /work/redmine/config/routes.rb:354)

ルーティング設定に:actionを使用するのは非推奨でRails 6.0で削除されるとあります。

Allowing :controller and :action values to be specified via the path in config/routes.rb has been an underlying cause of a number of issues in Rails that have resulted in security releases. In light of this it's
better that controllers and actions are explicitly whitelisted rather than trying to blacklist or sanitize 'bad' values.

簡単にまとめると

:actionの使用はセキュリティ上よろしくない問題をもたらすので、ルーティング設定ではアクションを明示的に指定すべき。

Redmine本体のroutes.rbの354行目は次の通りプラグインディレクトリのroutes.rbを評価しているコードなので、プラグイン側のroutes.rbの記述が警告対象となっています。

        instance_eval File.read(file) 

プラグインのroutes.rbから:actionを使用している行を抜粋します。

    match 'glossary_styles/:action', :controller => :glossary_styles, :via => [ :get, :post, :put, :patch ]
    match 'projects/:project_id/glossary/:id/:action', :controller => :glossary, :id => /\d+/, :via => :all
    match 'projects/:project_id/glossary/:action', :controller => :glossary, :via => :all
    match 'projects/:project_id/term_categories/:action', :controller => :term_categories, :via => :all
    match 'projects/:project_id/term_categories/:id/:action', :controller => :term_categories, :id => /\d+/, :via => :all

glossary_styles/:action の警告解消

まず最初のglossary_style/:action を検討します。ルーティング設定を確認すると次の通りです。

$ bundle exec rails routes
Prefix  Verb                URI Pattern                           Controller#Action
        GET|POST|PUT|PATCH  /glossary_styles/:action(.:format)    glossary_styles#:action

先に述べたように何でもアクションが呼ばれるのはセキュリティ上問題となるので、呼び出し可能なアクションを明示的に指定するように修正します。

そこで、まずglossary_stylesコントローラーに定義されるアクションを確認します。

  • search
  • edit

次に、それぞれのアクションがどのHTTPメソッドで呼ばれるかを調べます。searchは、GETで呼ばれますが、editはPOSTとPATCHの2つで呼ばれます。

リソース指向(RESTful)では、リソースの変更(edit)では、まずeditアクションをGETで呼び、updateアクションをPATCHで呼びます。しかし、元の用語集プラグインはeditアクションをPOSTおよびPATCHで呼ぶので、ルーティング設定でリソース指向のresource指定を使うのが難しく、別の記述をしています。

  get    '/glossary_styles/search', to: 'glossary_styles#search'
  post   '/glossary_styles/edit', to: 'glossary_styles#edit'
  patch  '/glossary_styles/edit', to: 'glossary_styles#edit'

このようにルーティング設定を書き換えた時の差分です。

match指定をやめて、HTTPコマンド指定に置き換え(Github)

テスト

Ruby on Railsにおけるテスト実施にあたり、テストツール(フレームワーク)として、Ruby標準搭載のMinitestと、RSpecとの2つがよく使われているようです。Redmineでプラグインの雛形を生成すると、前者のMinitest用のディレクトリとテストコードの雛形も合わせて生成されます。後者は、追加のgemを入れて自力でテストコードを作成していきます。

今回はMinitestでテストを記述します。

テストが実行できるようにするための修正

用語集プラグインのテストコードはRedmine 3.x時点においても実行するとエラーとなっていました。そこで、まずはテストが実行できるよう必要な修正をしていきます。

プラグインディレクトリ/test/test_helper.rbの修正

2つ修正をします。

まず1つ目です。Redmine本体のtestディレクトリにあるtest_helper.rbを参照していますが、ディレクトリ階層(相対ディレクトリ)が1つずれています。Redmine 1.xまではプラグインを置くディレクトリが、Redmineインストールディレクトリ/vendor/plugin/の下だった名残りと思われます。Redmine 2.0以降は、Redmineインストールディレクトリ/plugin/に変更されました。そこで、相対パスの階層を次のように修正します。

-require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper')
+require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')

2つ目は、プラグインのテストディレクトリにあるfixturesディレクトリ内のファイルをテストで使用できるようにするよう修正します。
元のコードにあるEngines::Testing.set_fixture_pathは、Redmine 1.x時代にRedmineインストールディレクトリ/vendor/plugins/enginesとして標準搭載されていたプラグインのAPIですが、Redmine 2.0で標準搭載プラグインがRedmineインストールディレクトリ/vendor/plugins/からRedmineインストールディレクトリ/lib/plugins/に移動になったときに落ちています。Railsの機能として取り込まれた模様です。修正は、書籍「Redmine Plugin Extension and Development」で紹介されているコードへ置き換えました。

-# Ensure that we are using the temporary fixture path
-Engines::Testing.set_fixture_path
+module Redmine
+  module PluginFixturesLoader
+    def self.included(base)
+      base.class_eval do
+        def self.plugin_fixtures(*symbols)
+          ActiveRecord::FixtureSet.create_fixtures(File.dirname(__FILE__) + '/fixtures/', symbols)
+        end
+      end
+    end
+  end
+end
+
+## ファンクショナルテスト
+unless ActionController::TestCase.included_modules.include?(Redmine::PluginFixturesLoader)
+  ActionController::TestCase.send :include, Redmine::PluginFixturesLoader
+end
+## ユニットテスト
+unless ActiveSupport::TestCase.included_modules.include?(Redmine::PluginFixturesLoader)
+  ActiveSupport::TestCase.send :include, Redmine::PluginFixturesLoader
+end
+## インテグレーションテスト
+unless Redmine::IntegrationTest.included_modules.include?(Redmine::PluginFixturesLoader)
+  Redmine::IntegrationTest.send :include, Redmine::PluginFixturesLoader
+end

テストコードの中で、fixtureファイルを使用するときはfixtureメソッドで指定しますが、上述のコード修正を適用した場合、Redmine本体のfixtureファイル(Redmineインストールディレクトリ/test/fixtures/内)を使用するときはそのままfixtureメソッドで指定、プラグインディレクトリ下のfixtureファイルを使用する際は、plugin_fixturesメソッドで指定することとなります。

 class GlossaryStyleTest < ActiveSupport::TestCase
-  fixtures :glossary_styles
+  plugin_fixtures :glossary_styles

ユニットテストの実行

$ bundle exec rails redmine:plugins:test:units NAME=redmine_glossary

現状ではfixtureファイルの不備(NOT NULL制約のカラムに値が未設定、カラム名のスペルミス、存在しないテーブル(モデル))があり、テスト実行はエラーになりますが、テストとしては実行可能な状態になっています。

ファンクショナル(機能)テストの実行

$ bundle exec rails redmine:plugins:test:functionals NAME=redmine_glossary
Run options: --seed 65303

# Running:

.....

Finished in 0.069078s, 72.3819 runs/s, 72.3819 assertions/s.
5 runs, 5 assertions, 0 failures, 0 errors, 0 skips

テストコードが実行されています。

インテグレーション(統合)テストの実行

$ bundle exec rails redmine:plugins:test:integration NAME=redmine_glossary
Run options: --seed 40146

# Running:



Finished in 0.003327s, 0.0000 runs/s, 0.0000 assertions/s.
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips

テストコードがないので0件となっています。

ユニットテスト

現状、存在しないモデルのテストコードとテストfixtureが存在しているので、これを削除します。

次に、NOT NULL制約付きカラムにデータ未設定のfixtureファイルがあり、値を設定しました。

次は不要なユニットテストコード・fixtureファイルの削除と、NOT NULL制約対応したときの差分です。

ユニットテストの不備修正(Github)

Rails 5でのMinitest

Redmine 5でのテスト(Minitest)関係の変更事項を調べてみました。直ちに実施しなくてもよさそうですが、今後を見据えて徐々に変更に対応していくのがよさそうです。

テストディレクトリの構造

Rails 5では次のテストディレクトリを生成します。

test
  +-- fixtures
  +-- models
  +-- controllers
  +-- integration

これまでのディレクトリ構造との対応は次となります。

従来 Rails 5
unit models
functional controllers
integration 同左

コントローラーのテスト

コントローラーのテストコードは、従来はActionController::TestCaseを継承していましたが、Rails 5の雛形生成されるコントローラーのテストではActionDispatch::IntegrationTestを継承するようになります。

IntegrationTestを継承すると、HTTPリクエストの呼び出し時にURLパスの指定が必要になります。従来のアクション名ではエラーとなってしまいました。

ActionController::TestCase を継承した従来の呼び方を、そのままActionDispatch::IntegrationTestを継承したテストクラスで記述すると、

  def test_index
    get :index, params: { project_id: 1 }
    assert_response :success
  end

次の様にエラーとなります。

Error:
TermCategoriesControllerTest#test_index:
URI::InvalidURIError: bad URI(is not URI?): http://www.example.com:80index
    plugins/redmine_glossary/test/functional/term_categories_controller_test.rb:7:in `test_index'

getの第1引数は、アクション名ではなく、URLパスを指定する必要があります。ルーティング設定でresourcesやresourceでルーティングを定義していれば、名前付きルートが生成されるのでそれを指定します。

名前付きルートがない場合は、直接パス文字列を指定します。

なお、Redmine 4.0(trunk)の雛形生成は従来通りActionController::TestCaseを継承するものです。

また、テスト用メソッドでassignsとassert_templateが削除されています。これを使ったテストは書き換えが必要となります。

積み残し

現状まだテストが通っていないので、テストを作って通す修正が残っています。オリジナルのプラグインにはテストは雛形生成されたものがそのまま置かれています。しかし、Railsのテストはまだ理解不足なので、少し時間をかけて取り組まないと書き表せない処で今後追記していきたいと思います。

次に、リリース作業(init.rb、ドキュメント)が手付かずです。こちらは、Redmine 4.0リリースには間に合わせたいです。

あとがきのようなもの

きっかけ

Redmine 1.xの頃に、用語集プラグインを見つけ、これはイイ!と使い始めました。その後、Redmineのメジャーバージョンアップ(Redmine 2.x、3.x)に対しては、用語集プラグインのオリジナル作者による対応はありませんでした。ですが、世の中には用語集プラグインをフォークしてRedmine 2.xに対応させている人が幾人かおりましたので、それらのフォーク版の中から一つを選んで利用しました。Redmine 3.xに際しては、2.x対応版をもとに、自分で泥縄的に対処しました。

その経緯は以前はてな日記に書きました。
http://d.hatena.ne.jp/torutk/20150321/p1

RubyもRailsもかじってみたレベルだったので、フォークして動かしエラーになったところを当てずっぽうに修正し、修正内容にまったく確信が持てていない状況でした。

Redmine 4.0対応への経緯

RedmineはRuby on Railsフレームワーク上で動作するアプリケーションとして作られています。Remine 3.xはRuby on Rails 4.2上で動きますが、来たるRedmine 4.0はRuby on Rails 5.2上で動きます。プラグインは、これまで動作してきたRuby on Rails 4.2から、新たにRuby on Rails 5.2で動くように対応が必要となります。

Railsがメジャーバージョンアップすると、かなりドラスティックに変化があります。

最初は、今年の4月に用語集プラグインをRedmine 4.0開発ブランチ(trunk/master)に入れて、出たエラーをつぶしていこうとしましたが、少なくはない変更、手に負えない変更があり、早々に挫折しました。挫折に至る経緯は次のはてな日記に書いています。

Redmine 4.0に対応させるには、Ruby力、Rails力、Redmine・プラグイン作成力を高める必要を痛感しました。そこで、まず、用語集プラグインをゼロから実装してみることでプラグインの作り方を学びました(5〜6月)。そのときの作業を次のWikiページにステップバイステップで書いています。

Redmine Glossaryプラグイン再構築

それから再度用語集プラグインのRedmine 4.0対応を開始しました。2か月の特訓(?)の成果で、今度は「分かるぞ、このエラー、直し方!」と自らの能力向上を感じました。Redmine 4対応の用語集プラグインのコードは次のGithubのブランチ上にあります。

16
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
19