2024年12月19日木曜日

Spring Data JPA の JPQL エラー備忘録(LocalDate がエラーになる)

Q: 以下エラーの原因がわかりますでしょうか?

Spring Data REST で、以下のコードを実装しました。

package dev.mikoto2000.study.springboot.web.practice20241215.repository;

import java.time.LocalDate;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import dev.mikoto2000.study.springboot.web.practice20241215.entity.BookMaster;
import dev.mikoto2000.study.springboot.web.practice20241215.projection.DefaultBookMasterProjection;

/**
 * BookMasterRepository
 */
@RepositoryRestResource(excerptProjection = DefaultBookMasterProjection.class)
public interface BookMasterRepository extends PagingAndSortingRepository<BookMaster, Long>, CrudRepository<BookMaster, Long> {

  @Query(value = """
    select b from BookMaster b
      where
        (b.id = :id or :id is null)
        and
        (b.name = :name or :name is null)
        and
        (b.publicationDate >= :publicationDateBegin or :publicationDateBegin is null)
        and
        (b.publicationDate <= :publicationDateEnd or :publicationDateEnd is null)
  """
  )
  Page<BookMaster> findByComplexConditions(
      Long id,
      String name,
      LocalDate publicationDateBegin,
      LocalDate publicationDateEnd,
      Pageable pageable);
}

しかし、これではクエリパラメーター publicationDateBegin=2024-12-19 を渡したときに以下のインターナルサーバーエラーとなります。何が原因でしょうか?

2024-12-19T12:49:28.873Z DEBUG 13187 --- [nio-8080-exec-8] o.s.web.servlet.DispatcherServlet        : Failed to complete request: org.springframework.dao.InvalidDataAccessResourceUsageException: JDBC ex
ception executing SQL [select bm1_0.id,bm1_0.name,bm1_0.ndc_category_id,bm1_0.publication_date from book_master bm1_0 where (bm1_0.id=? or ? is null) and (bm1_0.name=? or ? is null) and (bm1_0.publicati
on_date=? or ? is null) and (bm1_0.publication_date=? or ? is null) fetch first ? rows only] [ERROR: could not determine data type of parameter $6] [n/a]; SQL [n/a]
2024-12-19T12:49:28.874Z ERROR 13187 --- [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request p
rocessing failed: org.springframework.dao.InvalidDataAccessResourceUsageException: JDBC exception executing SQL [select bm1_0.id,bm1_0.name,bm1_0.ndc_category_id,bm1_0.publication_date from book_master 
bm1_0 where (bm1_0.id=? or ? is null) and (bm1_0.name=? or ? is null) and (bm1_0.publication_date=? or ? is null) and (bm1_0.publication_date=? or ? is null) fetch first ? rows only] [ERROR: could not d
etermine data type of parameter $6] [n/a]; SQL [n/a]] with root cause

org.postgresql.util.PSQLException: ERROR: could not determine data type of parameter $6
        at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2733) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2420) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:372) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:517) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:434) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:194) ~[postgresql-42.7.4.jar:42.7.4]
        at org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:137) ~[postgresql-42.7.4.jar:42.7.4]
        at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52) ~[HikariCP-5.1.0.jar:na]
        at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java) ~[HikariCP-5.1.0.jar:na]
        at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.executeQuery(DeferredResultSetAccess.java:250) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.getResultSet(DeferredResultSetAccess.java:171) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.<init>(JdbcValuesResultSetImpl.java:74) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.resolveJdbcValuesSource(JdbcSelectExecutorStandardImpl.java:355) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:137) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:102) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.exec.spi.JdbcSelectExecutor.executeQuery(JdbcSelectExecutor.java:91) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:165) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$1(ConcreteSqmSelectQueryPlan.java:152) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:442) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:362) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:380) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:143) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.hibernate.query.Query.getResultList(Query.java:120) ~[hibernate-core-6.6.2.Final.jar:6.6.2.Final]
        at org.springframework.data.jpa.repository.query.JpaQueryExecution$PagedExecution.doExecute(JpaQueryExecution.java:205) ~[spring-data-jpa-3.4.0.jar:3.4.0]
        at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:93) ~[spring-data-jpa-3.4.0.jar:3.4.0]
        at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:152) ~[spring-data-jpa-3.4.0.jar:3.4.0]
        at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:140) ~[spring-data-jpa-3.4.0.jar:3.4.0]
        at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:170) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:149) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0.jar:6.2.0]
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380) ~[spring-tx-6.2.0.jar:6.2.0]
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.2.0.jar:6.2.0]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0.jar:6.2.0]
        at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138) ~[spring-tx-6.2.0.jar:6.2.0]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0.jar:6.2.0]
        at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:136) ~[spring-data-jp
a-3.4.0.jar:3.4.0]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.0.jar:6.2.0]
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) ~[spring-aop-6.2.0.jar:6.2.0]
        at jdk.proxy2/jdk.proxy2.$Proxy150.findByComplexConditions(Unknown Source) ~[na:na]
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
        at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:281) ~[spring-core-6.2.0.jar:6.2.0]
        at org.springframework.data.repository.support.ReflectionRepositoryInvoker.invoke(ReflectionRepositoryInvoker.java:220) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.data.repository.support.ReflectionRepositoryInvoker.invokeQueryMethod(ReflectionRepositoryInvoker.java:155) ~[spring-data-commons-3.4.0.jar:3.4.0]
        at org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFactory$UnwrappingRepositoryInvoker.invokeQueryMethod(UnwrappingRepositoryInvokerFactory.java:97) ~[spring-data-rest-core
-4.4.0.jar:4.4.0]
        at org.springframework.data.rest.webmvc.RepositorySearchController.executeQueryMethod(RepositorySearchController.java:339) ~[spring-data-rest-webmvc-4.4.0.jar:4.4.0]
        at org.springframework.data.rest.webmvc.RepositorySearchController.executeSearch(RepositorySearchController.java:170) ~[spring-data-rest-webmvc-4.4.0.jar:4.4.0]
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
        at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.2.0.jar:6.2.0]
        at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1088) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:978) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.33.jar:6.0]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.2.0.jar:6.2.0]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.33.jar:6.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.2.0.jar:6.2.0]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.0.jar:6.2.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.2.0.jar:6.2.0]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.0.jar:6.2.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.2.0.jar:6.2.0]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.0.jar:6.2.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
        at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:

A: キャストが足りなかったらしい

null と突き合わせるときは明示的に型指定が必要っぽい (date(:publicationDateBegin) is null とか date(:publicationDateEnd) is null の部分)

package dev.mikoto2000.study.springboot.web.practice20241215.repository;

import java.time.LocalDate;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import dev.mikoto2000.study.springboot.web.practice20241215.entity.BookMaster;
import dev.mikoto2000.study.springboot.web.practice20241215.projection.DefaultBookMasterProjection;

/**
 * BookMasterRepository
 */
@RepositoryRestResource(excerptProjection = DefaultBookMasterProjection.class)
public interface BookMasterRepository extends PagingAndSortingRepository<BookMaster, Long>, CrudRepository<BookMaster, Long> {

  @Query(value = """
    select b from BookMaster b
      where
        (b.id = :id or :id is null)
        and
        (b.name = :name or :name is null)
        and
        (b.publicationDate >= :publicationDateBegin or date(:publicationDateBegin) is null)
        and
        (b.publicationDate <= :publicationDateEnd or date(:publicationDateEnd) is null)
  """
  )
  Page<BookMaster> findByComplexConditions(
      Long id,
      String name,
      LocalDate publicationDateBegin,
      LocalDate publicationDateEnd,
      Pageable pageable);
}

2024年12月16日月曜日

Vim 本体に evalfunc (Vim script 関数) を追加する

この記事は Vim 駅伝 の 2024/12/16 の記事です。 前回の記事は yuys13 さんによる、 2024/12/13 の「ソフトウェア技術者とコミュニティ活動~vim-jp radioに出演しました~」という記事でした。

次回は 2024/12/18 に投稿される予定です。

この記事はなに?

少し前に Vim script 関数を追加する Pull Request を出したので、その流れを忘れないように記録するための記事です。

VimConf 2024 の Kato さんのセッションとかぶってしまっていますが、実装の一例として見ていただくのがいいかと思います。 セッションの方がより深掘りされているので、視聴がおすすめです。

今回は、足し算をする関数を追加してみる。

前提

  • Vim のビルド環境が整った状態からスタート

手順概要

  1. 関数の実体を実装
  2. 関数のエントリーポイントを追加
  3. ドキュメントを追加
    1. 関数の説明を追加
    2. タグを追加
    3. 関数リストに追加
  4. 関数のテストを実装

実装

関数の実体を実装

evalfunc.c:

一番下に、実装する。

diff --git a/src/evalfunc.c b/src/evalfunc.c
index b2905da2a..99527029e 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -12125,3 +12125,9 @@ f_xor(typval_T *argvars, typval_T *rettv)
 }
-
 #endif // FEAT_EVAL
+
+    int
+add_num(int x, int y)
+{
+    return x + y;
+}

関数のエントリーポイントを追加

引数パターンの確認

今回追加するのは、ふたつの引数がそれぞれ「数値」、「数値」の引数を持つ関数。

Lists of functions that check the argument types of a builtin function. と コメントされている行から下に、引数のパターンごとに変数がつくられている。

今回追加したいパターンの引数の組み合わせは arg2_number[] となる。

エントリーポイント関数の追加

evalfunc.c:

今回は、さきほど実装した「関数の実体」の直後にエントリーポイントを実装し、プロトタイプ宣言も追加する。

diff --git a/src/evalfunc.c b/src/evalfunc.c
index 99527029e..8fcdaaf60 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -194,6 +194,7 @@ static void f_wildmenumode(typval_T *argvars, typval_T *rettv);
 static void f_windowsversion(typval_T *argvars, typval_T *rettv);
 static void f_wordcount(typval_T *argvars, typval_T *rettv);
 static void f_xor(typval_T *argvars, typval_T *rettv);
+static void f_add_num(typval_T *argvars, typval_T *rettv);
 
 
 /*
@@ -12131,3 +12131,26 @@ add_num(int x, int y)
 {
     return x + y;
 }
+
+/*
+ * "add_num(number, number)" function
+ */
+    static void
+f_add_num(typval_T *argvars, typval_T *rettv)
+{
+    // argvars[] の第一引数と第二引数の型チェック
+    if (in_vim9script()
+           && (check_for_number_arg(argvars, 0) == FAIL
+               || check_for_number_arg(argvars, 1) == FAIL))
+       return;
+
+    // argvars[] から第一引数と第二引数を取得
+    int x = tv_get_number_chk(&argvars[0], NULL);
+    int y = tv_get_number_chk(&argvars[1], NULL);
+
+    // 戻り値に計算結果を格納
+    rettv->vval.v_number = add_num(x, y);
+
+    return;
+}
+

エントリーポイント定義の追加

evalfunc.c:

1758 行目くらいからあるエントリーポイントの定義に、今回実装したエントリーポイントの定義を追加する。

diff --git a/src/evalfunc.c b/src/evalfunc.c
index 99527029e..d51b6ac1f 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -1758,6 +1758,8 @@ static funcentry_T global_functions[] =
                        ret_float,          f_acos},
     {"add",            2, 2, FEARG_1,      arg2_listblobmod_item,
                        ret_first_arg,      f_add},
+    {"add_num",        2, 2, 0,            arg2_number,
+                       ret_number,         f_add_num},
     {"and",            2, 2, FEARG_1,      arg2_number,
                        ret_number,         f_and},
     {"append",         2, 2, FEARG_2,      arg2_setline,

各定義は以下の通り。

  1. 関数名
  2. 最小引数数
  3. 最大引数数
  4. メソッドとして利用できる引数の位置
  5. 引数の組み合わせ
  6. 戻り値の型
  7. 実際に呼び出すエントリーポイント

ドキュメントを追加

関数の説明を追加

runtime/doc/builtin.txt に関数のドキュメントを辞書順になるように追加する。

diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index d0f0c7b03..7370091a0 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -26,6 +26,7 @@ USAGE                         RESULT  DESCRIPTION     ~
 abs({expr})                    Float or Number  absolute value of {expr}
 acos({expr})                   Float   arc cosine of {expr}
 add({object}, {item})          List/Blob   append {item} to {object}
+add_num({number}, {number})    Number  add two number
 and({expr}, {expr})            Number  bitwise AND
 append({lnum}, {text})         Number  append {text} below line {lnum}
 appendbufline({buf}, {lnum}, {text})
@@ -833,6 +834,12 @@ add({object}, {expr})                                      *add()*
                |Blob|
 
 
+add_num({number}, {number})                            *add_num()*
+               Add two {number}.
+
+               Return type: |Number|
+
+
 and({expr}, {expr})                                    *and()*
                Bitwise AND on the two arguments.  The arguments are converted
                to a number.  A List, Dict or Float argument causes an error.

タグを追加

runtime/doc/tags に関数のドキュメントのタグを辞書順になるように追加する。

diff --git a/runtime/doc/tags b/runtime/doc/tags
index f33988020..f92a144df 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -5995,6 +5995,7 @@ ada-extra-plugins ft_ada.txt      /*ada-extra-plugins*
 ada-reference  ft_ada.txt      /*ada-reference*
 ada.vim        ft_ada.txt      /*ada.vim*
 add()  builtin.txt     /*add()*
+add_num()      builtin.txt     /*add_num()*
 add-filetype-plugin    usr_05.txt      /*add-filetype-plugin*
 add-global-plugin      usr_05.txt      /*add-global-plugin*
 add-local-help usr_05.txt      /*add-local-help*

関数リストに追加

runtime/doc/usr_41.tx に関数定義を辞書順になるように追加。

diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index 36907d249..29e6d2ffc 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -808,6 +808,7 @@ List manipulation:                                  *list-functions*
        empty()                 check if List is empty
        insert()                insert an item somewhere in a List
        add()                   append an item to a List
+       add_num()               add two number
        extend()                append a List to a List
        extendnew()             make a new List and append items
        remove()                remove one or more items from a List

関数のテストを実装

今回は、ただの関数なので src/testdir/test_functions.vim にテスト関数を追加する。

diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim
index 8b2518f2b..9167b2952 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -4206,4 +4206,9 @@ func Test_getcellpixels_gui()
   endif
 endfunc
 
+func Test_add_num()
+  let result = add_num(1, 2)
+  call assert_equal(3, result)
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab

make test_functions で、このファイル内のテストのみを実行できる。

参考資料

2024年12月10日火曜日

Vim の組み込み関数の結果をバッファに出力する

この記事は Vim advent calendar 2024(Adventar) の10日目の記事です。

この記事はなに?

getcellpixels() の追加を行った際に、 テストでバッファに関数の結果を出力する必要があったので、やり方を調べた。

結論

:redi @"
:echo getcellpixels()
:redi END
""p

ここでは、 :redi @" で、コマンドの出力を " レジスタへリダイレクト開始の設定をしている。

その後、 :echo getcellpixels()getcellpixels() 関数の結果を出力しているが、 先のリダイレクト設定により、その結果が " レジスタに格納される。

:redi END でリダイレクト設定を解除し、 ""p" レジスタの内容をバッファにペーストする。

こうすることで、空のバッファにペーストした際には、バッファの 3 行目にコマンドの結果が出力される。

3 行目である理由は謎。

以上。

参考資料

2024年12月6日金曜日

keyinput-delayer.vim で Vim での編集力を鍛える

この記事は Vim advent calendar 2024(Adventar) の4日目の記事です。

この記事はなに?

mikoto2000/keyinput-delayer.vim: 別名「Vimmer 養成ギプス」。 を作ったので、その宣伝です。

人はついつい「知っている方法・慣れた方法」で物事を進めてしまう。

それは Vim における編集でも同じことで、ついつい h, j, k, l での移動をしてしまいがちです。

それはなぜかというと、「キー入力のコストがとても低いから」であると私は考えました。

キー入力のコストがもっと高ければ、丁寧丁寧丁寧に操作を吟味してキー入力をすることでしょう。

例えば、帯域の狭い回線の SSH で、エコーバックがなかなか帰ってこない時、いつもより丁寧に操作をしたことはありませんか?

そんな環境を Vim 上で再現するのが mikoto2000/keyinput-delayer.vim, 別名「Vimmer 養成ギプス」です。

使い方

let g:keyinput_delayer_delay_time = "1000m" のようにディレイ時間を設定し、 keyinput_delayer#ToggleKeyInputDelay() を呼び出すことで、ディレイのあり・なしを切り替えます。

例えばディレイを 1 秒にしたとき、 10 行下に移動するために必要な時間は以下のようになります。

  • j x 10 : 10 秒
  • 10j : 3 秒

これを見て分かる通り、キーインプットのコストが非常に高くなるため、 「タイプ数を減らして編集を実現する」という意識が高くなります。

皆さんも keyinput-delayer.vim を導入して Vim のキーストロークゴルフ思考を身に着けていきましょう!

類似プラグイン

同じ目的のプラグインが存在することを vim-jp で教えていただきました。

こちらは、連続の 1 マス移動でカーソルが全く動かなくなるので、 よりストイックに鍛えたい方に向いているのではないでしょうか?

以上、 keyinput-delayer.vim の宣伝でした。

2024年12月3日火曜日

Nix で Vim をスタティックリンクビルドする。ついでにクロスコンパイルもする

この記事はなに?

mikoto2000/devcontainer.vim の ARM 対応で、 ARM 版のスタティックリンクな Vim が必要になった。

vim-jp にて「Nix ならコマンド一発でできるよ」と教えてもらったのでやってみた。

前提

  • Docker: Docker version 27.3.1, build ce12230
  • 使用イメージ: nixos/nix

Nix のリポジトリアップデート

nix-channel --update

スタティックリンクビルド

nix-build '<nixpkgs>' --cores 20 -A pkgsStatic.vim
  • --cores: make でいうところの -j オプション

aarch64 のクロスコンパイル&スタティックリンクビルド

nix-build '<nixpkgs>' --cores 20 -A pkgsCross.aarch64-multiplatform.pkgsStatic.vim

ビルドされた Vim の場所

find で探しましょう。

bash-5.2# find /nix -name vim
/nix/store/jx5d1dp77cwkarvalh1l8zvfp2ziqzlw-vim-static-aarch64-unknown-linux-musl-9.1.0787/share/vim
/nix/store/jx5d1dp77cwkarvalh1l8zvfp2ziqzlw-vim-static-aarch64-unknown-linux-musl-9.1.0787/bin/vim
/nix/store/7wkl9dji0dq6v7nmdjp1d72wd5pydqpb-vim-static-x86_64-unknown-linux-musl-9.1.0787/share/vim
/nix/store/7wkl9dji0dq6v7nmdjp1d72wd5pydqpb-vim-static-x86_64-unknown-linux-musl-9.1.0787/bin/vim
/nix/store/gnzqq5zg8r55apxa5avlb5yp0ix8qdwk-nixpkgs/nixpkgs/pkgs/applications/editors/vim
/nix/store/gnzqq5zg8r55apxa5avlb5yp0ix8qdwk-nixpkgs/nixpkgs/pkgs/test/vim
/nix/store/yzqnkb2mfzphmhv3248pw0pqh6f1mn6y-7sprarsdfz9qcd7859phvr9nvhi14mri-source/pkgs/applications/editors/vim
/nix/store/yzqnkb2mfzphmhv3248pw0pqh6f1mn6y-7sprarsdfz9qcd7859phvr9nvhi14mri-source/pkgs/test/vim

bin 下のが目的のバイナリですね。

share の方にはランタイムが入っています。

パッケージング

Vim は、ランタイムと一緒に配布しないと上手く動かないので、パッケージングを行う。 さらに、ランタイムの場所はビルド時に決め打ちされるため、 VIM 環境変数を上書きして Vim を起動するシェルスクリプトを用意し、それ経由で実行してもらうようにする。 (今回の場合は AppRun というシェルスクリプトを用意)

# aarch64 のパッケージング
cp -a /nix/store/jx5d1dp77cwkarvalh1l8zvfp2ziqzlw-vim-static-aarch64-unknown-linux-musl-9.1.0787 vim-9.1.0787-aarch64
cd vim-9.1.0787-aarch64
cat << "EOF" > ./AppRun
#!/bin/sh
CURRENT_DIR="$(dirname "$(realpath "$0")")"
export VIM="${CURRENT_DIR}/share/vim/"
exec "${CURRENT_DIR}/bin/vim" "$@"
EOF
chmod a+x ./AppRun
cd ..
tar zcfv ./vim-9.1.0787-aarch64.tar.gz vim-9.1.0787-aarch64

# aarch64 のパッケージング
cp -a /nix/store/7wkl9dji0dq6v7nmdjp1d72wd5pydqpb-vim-static-x86_64-unknown-linux-musl-9.1.0787 vim-9.1.0787-x86_64
cd vim-9.1.0787-x86_64
cat << "EOF" > ./AppRun
#!/bin/sh
CURRENT_DIR="$(dirname "$(realpath "$0")")"
export VIM="${CURRENT_DIR}/share/vim/"
exec "${CURRENT_DIR}/bin/vim" "$@"
EOF
chmod a+x ./AppRun
cd ..
tar zcfv ./vim-9.1.0787-x86_64.tar.gz ./vim-9.1.0787-x86_64

これで、「ダウンロードした tar.gz を展開して、その中の AppRun を実行すれば vim が起動する」という感じになります。

devcontainer.vim で必要なので、このスタティックリンクな Vim を mikoto2000/vim-static: Distributing a static link binary for vim/vim. で配布予定です。

以上。

参考資料

2024年11月30日土曜日

macOS で nexe を使って devcontainers/cli をシングルバイナリにする

前提

  • OS: macOS Sequoia 15.1.1 (24B91)
  • Homebrew インストール済み

前の macOS セットアップ記事の続きから。

Node.js のインストール

brew install nvm
mkdir ~/.nvm
export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"  # This loads nvm
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"  # This loads nvm bash_completion
nvm install v21.7.3

devcontainers/cli のクローン

git clone --depth 1 -b v0.72.0 https://github.com/devcontainers/cli

ビルド

cd cli
yarn
yarn compile-prod
npx nexe --python=/usr/bin/python3 --loglevel="verbose" -i ./devcontainer.js --make="-j8" -r="scripts/updateUID.Dockerfile" -t -b -o ../dist/devcontainer-darwin-arm64-v0.72.0

node 自体に手を入れるらしく、初回に node 自体のビルドが走ります。

マシンスペックによりますが、初回ビルドは 1 時間以上かかることもあるので気長に待ちましょう。

ビルド後の node は保存されるので、 2 回目以降は一瞬です。

ファイルを埋め込んでアクセスしたいなどが無ければ Node 自体の Single Binary Application の仕組みを使う方ががおすすめです(これはこれで新しい node じゃないと無理ですが…)

以上。

おまけ

上記手順でシングルバイナリにしたモノを、以下リポジトリで配布しています。

mikoto2000/devcontainers-cli: Distributing a single executable binary for devcontainer/cli.

参考資料

変更履歴

日付 内容
2024/11/30-1 新規作成
2024/11/30-2 nexe の注意点について追記

macOS で Nodejs をビルドする

前提

  • OS: macOS Sequoia 15.1.1 (24B91)
  • Homebrew インストール済み

前回の macOS セットアップ記事の続きから。

Nodejs のクローン

git clone --depth 1 -b v23.3.0 https://github.com/nodejs/node

ビルド

cd node
./configure
make -j8

以上。

参考資料

2024年11月29日金曜日

M1 mac mini を買ったのでセットアップしていく

この記事はなに?

作っているアプリの macOS での動作確認を行うために M1 Mac を買った。

それのセットアップ記録。

やりたいのは以下。

  • SSH 接続で作業
    • ホスト名で接続したい
  • Docker を使いたい

前提

インストール・初回起動時のセットアップは省略。

  • OS: macOS Sequoia 15.1.1(24B91)

SSH 接続

SSH 接続の有効化

  1. システム設定 -> 一般 -> 共有 -> リモートログイン を ON にする

ホスト名変更

システム設定 -> 一般 -> 共有 -> ローカルホスト名 を変更

ホスト名で接続できるようにする

Windows のネットワーク共有を ON にする。

これで、 Windows の SSH から macOS へホスト名で SSH 接続できるようになった。

Docker のインストール

※ Docker Desktop は SSH 接続のみで使うのに向いていなかったので abiosoft/colima: Container runtimes on macOS (and Linux) with minimal setup を使う事とした。

Applie silicon の Mac - Mac に Docker Desktop をインストール — Docker-docs-ja 24.0 ドキュメント の手順通りに実施。

Rosetta 2 のインストール

macOS のターミナルで、以下コマンドを実行。(SSH 越しにやったらエラーになった…)

softwareupdate --install-rosetta

Docker Desktop for Mac のダウンロード

Get Docker | Docker Docs から Docker Desktop for Mac -> Docker Desktop for Mac with Apple silicon を選択。

docker.dmg がダウンロードされるので、ダブルクリック -> Docker のアイコンを Applications のアイコンにドラッグアンドドロップ。

そのまま Applications のアイコンをダブルクリック -> Docker のアイコンをダブルクリック。

Accept したり、 Use recommended settings を選んだり、 Docker にログインしたりして完了。

Colima のインストール

brew install colima
brew install docker

これで、 colima start 後に docker コマンドが使えるようになる。

その他開発ツールのインストール・設定

GitHub

GitHub CLI のインストール

brew install gh

GitHub に公開鍵を登録

gh auth login

表示されたガイド通りに進めれば OK.

Vim

インストール

brew install vim

dotfile 取得

git clone --recurse git@github.com:mikoto2000/dotvim ~/.vim

参考資料

変更履歴

日付 修正内容
2024/11/29-1 新規作成
2024/11/29-2 「その他開発ツールのインストール・設定」を追加
2024/12/03-1 Docker Desktop for Mac を止めて Colima にしたことを追記

2024年11月25日月曜日

VimConf 2024 で登壇してきました

この記事はなに?

VimConf 2024 の参加記です。

参加レポートも、「登壇者にしか書けないもの」を求められている空気を感じたので、自分語りをやっていきます。

プロポーザルを出したきっかけ

「タイミングが良かった」に尽きる。

devcontainer.vim - VSCode Dev Container の Vim 版。 を 3 月ごろから作り始めて、9 割満足できるくらい出来上がったところで VimConf 2024 の CfP が公開された。

「devcontainer.vim 自体はただのコマンドラインツールで、作った周辺ツールもただのコマンドラインツール なので Vim の話はほとんどで無いけどいいのかな?」と少し悩んだが、 「まー、僕の考えた最強の Vim 開発環境だし」と思い結局エイヤで出してしまった。

実際に出したプロポーザル

Vim Conf 2024 CfP のやつ
---
title: Vim Conf 2024 CfP のやつ
author: mikoto2000
date: 2024/7/13
---

mikoto2000@gmail.com



mikoto2000



https://raw.githubusercontent.com/mikoto2000/TIL/master/svg/icon/myicon.svg



Creating the Vim Version of VSCode Dev Container Extension: Why and How



Vim is great. Containers are great too. But combining them is not as easy as VSCode.
We have created a few tools to make it easier to combine them for development.
I will talk about why I made the tool, its overview, how it works, and the challenges I faced.


# 自身について

## 概要

仕事と趣味でプログラミングをやっているプログラマー。
一生を Getting Started の実施で過ごす人。
最近は [Tauri](https://tauri.app/) のベータ版でアプリを作りながら issue や Pull Request を出す生活をしています。

## Vim 活

「自身の使うプラグインは、できるだけ自身がつくったプラグインで済ませる」という挑戦をしている
 LSP や補完系のものはさすがにあきらめたが、その他小粒なツールは自作している

- ファイルエクスプローラー
- バッファーセレクター
- ファイルファインダー
- スニペットジャンプ


# 講演の概要

コンテナ内で Vim を使った開発をするためのツールを作成しました。
(「VSCode Dev container 拡張機能の Vim 版」と放言しています)
そのツールを何故作ったのか?・概要・仕組み・苦労話を話します。
https://github.com/mikoto2000/devcontainer.vim

Vim と何かをインテグレーションする際の一例として、誰かに刺されば良いかなぁと思っています。


# 話すこと

・何を作ったのか?
(仕組みの比較のため)VSCode Dev Container 拡張機能について。
作ったもの(devcontainer.vim)の仕組み説明

・何故作ったのか?
なんでコンテナ内で Vim を使いたいのか?

・解決したい課題と解決方法
初期の方法とその課題
課題を解決するために取った方法
(ツールの作成と、インテグレーション)
どんな方法でどんな課題を解決したのかの説明。

・(時間があれば)作る際の苦労話をいくつか

- NeoVim移行を考えてあきらめた話
- Vim に Pull Request を送った話

# 話さないこと

以下 2 点以外の Vim の機能全て

- channel による TCP 通信
- コマンドライン引数 `-S` による Vim Script の実効


20, 15



Japanese



English


Yes

書き始めたのは 7/13 だったらしい。実際出したのはもっと後のはず…

採択されてから

CfP を出した時点で章立てがほぼ決まっていたので、それに合わせてがりがりとパワーポイントを書いていった。 図を盛り盛りに盛り込んでインクで印をつけながら説明していくスタイル。

結局発表時間が足りなかったので、「時間があったら話すこと」は省略した。

Slack のログを見ると、スライド自体は 10/21 に提出、英語表現や図の表現に関してレビュー・指摘を受けて 10/23 に再提出したようだ。

スピーカーノートまで含めた最終版は 11/6 に提出。

その後は時間を計りながらしゃべりの練習(合計 10 回くらい?)をして、当日を迎えた。

ちょっと練習足らんかったな…。緊張しててもカンペなしでしゃべれるくらいまでやらないと。

当日の様子

登壇前

死ぬほど緊張していた。

登壇

死ぬほど緊張していましたが、スピーカーノートで一命をとりとめました。カンペ大事。

スピーカーノート通りにしゃべれなかった箇所があったり、改善点はあれど、あの時点でのベストは尽くした…

登壇後

魂が抜けた…

席に戻る途中で TJ 氏に「Good session」(うろ覚え)と声をかけてもらえてとてもうれしかった。

懇親会

端っこで人見知りしていたが、何人かの方が話しかけてくれたり、 ツールの仕組みや Dev container に興味を持って質問してくれたりして嬉しかった。

登壇で話せなかったこと

NeoVim に移行しようとしてあきらめた

  • NeoVim は、VSCode と同じく、 UI と実処理をクライアント/サーバーに分けて起動できる
  • クリップボード連携にとても都合の良い構成なので、この機会に NeoVim に移行し、クライアント/サーバー構成を利用しようとした
  • しかし、利用している自作プラグインが、 job_start を多用しており NeoVim で動かなかった
  • せっかくなら Vim/NeoVim 両対応しようと思って修正していたら、別案の clipboard-data-receiver の仕組みの方が先にできてしまった…

Vim に Pull Request を出した話

  • devcontainers が公開している Docker イメージでは、プロンプトに が使われている
  • ターミナル版 Vim では、 ambiwidth の設定にかかわらず が半角分の領域で描画される
  • が描画された行では、リフレッシュが上手く行われず画面にゴミが残りがちになる
  • これが我慢ならずに Vim に改善の Pull Request を出した

なんでここまでしたの?

VSCode Dev Container 拡張機能の Vim 版という事なんですが、

  1. 「VSCode で Dev container が使えるのがうらやましい → コンテナに Vim 突っ込もう」から始まり、
  2. 「コンテナ内で快適に開発したい」となり、
  3. 「周りが VSCode な中で、どうにかこっそり Vim で Dev container したい」になり、
  4. そんなこんなで「Vim で Dev container を利用した開発ができる、かつ、既存 VSCode ユーザーの邪魔にならないようにする」

という仕組みが出来上がった。

devcontainer って便利なの?

この辺りにメリットを感じる人にはフィットするかも。

  • 「開発環境 as Code」ができる
    • 開発環境の構成が devcontainer.json を見ればわかるし、 devcontainer が自動で解釈してくれるので Docker さえインストールしていれば、開発環境の再現が容易になる
  • docker compose ファイルにも対応しているので、複数サーバーが必要な開発環境も構築できる
  • VSCode Dev 拡張機能で利用されている資産を利用できる

devcontainer.vim って便利なの?

devcontainer のメリットに以下をプラスした感じ。 この辺りにメリットを感じる人にはフィットするかも。

  • いつものイメージにそのまま Vim を突っ込めるので、 Dockerfile の作成が不要
  • LS って大体ランタイムと同じ言語で実装されているので、 何も考えずにコンテナ上に LS をインストールできることが多い (私は、コンテナを立ち上げるたびに vim-lsp-settings で LS をインストールしている)

※ Vim から出れないので、 Vim の :sh:terminal を使いこなせる必要がある

最後に

意外と話したいけど話せなかったことがいっぱいあったな…

devcontainer.vim - VSCode Dev Container の Vim 版。、説明できなかったこまごました機能もあるので、一度 README を読んでみてください!

何か質問があれば @mikoto2000 でもこのブログのコメントでもなんでもよいので聞いていただければと思います。

参考資料

2024年10月28日月曜日

WezTerm 上の Vim で ALT 系のマッピングを使いたい

この記事は Vim 駅伝 の 2024/10/28 の記事です。 前回の記事は staticWagomU さんによる、 2024/10/25 の「個人的にいいと思うVim_Neovimの始め方」という記事でした。

次回は 2024/10/30 に投稿される予定です。

この記事はなに?

急に wsltty が使えなくなり、復旧もできない状態になってしまった。

そのため、代替ターミナルアプリを探していたところ、 「ほとんどのターミナルアプリでは <ALT-*><C-.> が効かない」 という事がわかり絶望した。 (VSCode 互換マッピングとして、<A-S-f><C-.> を使っていた)

そんな中、kuuote さんから「WezTerm ならキーバインディングの設定でできるよ」と 教えていただき、無事 <ALT-f><C-.> のマッピングを呼び出すことに成功したので何をやったか書いていく。

結論

以下設定を .wezterm.lua に設定することで行けた。

config.keys = {
  { -- <C-.>
    key=".",
    mods="CTRL",
    action=wezterm.action.SendString('\x1b[46;5u')
  },
  { -- <A-S-F> ( `F` の 8 bit 目を 1 にした文字を送信)
    key="F",
    mods="META|SHIFT",
    action=wezterm.action.SendString('Æ')
  }
}

<C-.> のマッピング

showkey -a で出力された文字列を SendString で送信することで、 Vim にキーコードが届けられる。 こちらも kuuote さんから教えていただきました。ありがとうございます。

<A-S-f> のマッピング

コメントにも書いてありますが、こちらは「F の 8 bit 目を 1 にした文字を送信」することで、 Vim に「Alt と F が同時押しされましたよ」と伝えられる。

以下引用のように、 Vim では「文字の 8 bit 目が 1 の時に Alt と同時押しされていると認識する」ようになっている。

初期設定では、ALT キーが押されているときは、文字の 8 ビット目が設定されるもの と仮定しています。xterm、aterm、rxvt など、ほとんどの端末はこの方法で問題あり ません。<A-k> のようなマップが動作しない場合は、その端末が、文字の前に ESC を 付けているのかもしれません。

1.10 ALT キーを使ったマップ - map - Vim日本語ドキュメント より

ほとんどのターミナルアプリ(GNOME Terminal, Windows Terminal, WezTerm, mintty など)は、 「文字の前に ESC を付ける」事で ALT と同時押しを表現しているため、これらの端末を使っている場合、 ALT 系のマッピングを処理できない。

今回は F にマッピングしたかったので F の文字コード 70 (0100 0110) の 8 bit 目を 1 にした Æ (文字コード 198(1100 0110)) を送信している。

現状

WezTerm にこの設定を行うことで、現状のマッピングで困ることはあまりなく快適にコーディングできている。

このまま wsltty の復旧がかなわなければ、 WezTerm を使っていくことになるだろう。

以上。

参考資料

2024年10月9日水曜日

Rails の Devise で認証を実現したプロジェクトに、 Pundit で認可を追加する

前提

Rails の Devise で認証を実現する からの続き。

ロールが adminuser で取得できるリソースを分ける。

アカウントに role カラムを追加

マイグレーションファイルを作成

rails generate migration add_role_to_accounts

マイグレーションファイルで、 role カラムを追加

db/migrate/20241008113604_add_role_to_accounts.rb:

class AddRoleToAccounts < ActiveRecord::Migration[7.2]
  def change
    add_column , , , "user"
  end
end

マイグレーション実行

rails db:migrate

管理者は、 DB から直接 role カラムを修正することとし、ログインのビューは変更しない。

リソースの作成

一般ユーザーも触れるリソースの追加

rails generate scaffold AllWelcomeResource name:string
rails db:migrate

管理者のみ触れるリソースの追加

rails generate scaffold AdminOnlyResource name:string
rails db:migrate

トップ画面の更新

各リソースの index へ行けるように、トップ画面にリンクを作成。

app/views/top/index.html.erb:

<ul>
  <li>一般歓迎リソース</li>
  <li>
    <ul>
      <li>
        <%= link_to :all_welcome_resources, all_welcome_resources_path %>
      </li>
    </ul>
  </li>
  <li>管理者リソース</li>
  <li>
    <ul>
      <li>
        <%= link_to :admin_only_resources, admin_only_resources_path %>
      </li>
    </ul>
  </li>
</ul>

Pundit ジェムのインストール

bundle add pundit
rails generate pundit:install

AdminOnlyResource への認可処理追加

ざっくり手順は、以下。

  1. コントローラーに認可処理に必要なボイラープレートを記載
  2. ポリシーファイルに index, show, create, new, update, edit, destroy の 7 種類に対する認可ポリシーを記述する。

コントローラーにボイラープレートを記載

ApplicationController

今回は、 users テーブルではなく accounts テーブルを使って認証を行っているため、ログイン済みユーザーの取得には current_account 関数を使う必要がある。

Pundit のデフォルトでは、 current_user 関数を使う用になっているため、この定義を上書きする。

app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  include Pundit    # この 4 行を追加
  def pundit_user   # この 4 行を追加
    current_account # この 4 行を追加
  end               # この 4 行を追加

  before_action 
  # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
  allow_browser 
end

AdminOnlyResourceController

app/controllers/admin_only_resources_controller.rb:

class AdminOnlyResourcesController < ApplicationController
  include Pundit                                                      # この 2 行を追加
  rescue_from Pundit::NotAuthorizedError,   # この 2 行を追加

  before_action , %i[ show edit update destroy ]

  # GET /admin_only_resources or /admin_only_resources.json
  def index
    authorize AdminOnlyResource # この行を追加
    @admin_only_resources = AdminOnlyResource.all
  end

  # GET /admin_only_resources/1 or /admin_only_resources/1.json
  def show
    authorize AdminOnlyResource # この行を追加
  end

  # GET /admin_only_resources/new
  def new
    authorize AdminOnlyResource # この行を追加
    @admin_only_resource = AdminOnlyResource.new
  end

  # GET /admin_only_resources/1/edit
  def edit
    authorize AdminOnlyResource # この行を追加
  end

  # POST /admin_only_resources or /admin_only_resources.json
  def create
    authorize AdminOnlyResource # この行を追加
    @admin_only_resource = AdminOnlyResource.new(admin_only_resource_params)

    respond_to do |format|
      if @admin_only_resource.save
        format.html { redirect_to @admin_only_resource, "Admin only resource was successfully created." }
        format.json { render , , @admin_only_resource }
      else
        format.html { render ,  }
        format.json { render @admin_only_resource.errors,  }
      end
    end
  end

  # PATCH/PUT /admin_only_resources/1 or /admin_only_resources/1.json
  def update
    authorize AdminOnlyResource # この行を追加
    respond_to do |format|
      if @admin_only_resource.update(admin_only_resource_params)
        format.html { redirect_to @admin_only_resource, "Admin only resource was successfully updated." }
        format.json { render , , @admin_only_resource }
      else
        format.html { render ,  }
        format.json { render @admin_only_resource.errors,  }
      end
    end
  end

  # DELETE /admin_only_resources/1 or /admin_only_resources/1.json
  def destroy
    authorize AdminOnlyResource # この行を追加
    @admin_only_resource.destroy!

    respond_to do |format|
      format.html { redirect_to admin_only_resources_path, , "Admin only resource was successfully destroyed." }
      format.json { head  }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_admin_only_resource
      @admin_only_resource = AdminOnlyResource.find(params[])
    end

    # Only allow a list of trusted parameters through.
    def admin_only_resource_params
      params.require().permit()
    end

    def user_not_authorized                                             # この 4 行を追加
      flash[] = "You are not authorized to perform this action."  # この 4 行を追加
      redirect_to(request.referer || root_path)                         # この 4 行を追加
    end                                                                 # この 4 行を追加
end
  • include Pundit: Pundit の機能を使えるようにする
  • rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized: 認証失敗時にトップページへリダイレクト
  • authorize <モデル名>: 認可処理、認可に失敗すると、 Pundit::NotAuthorizedError が発生する

ポリシーファイルに index, show, create, new, update, edit, destroy の 7 種類に対する認可ポリシーを記述する。

今回は、 AdminOnlyResource のポリシーを作成するので、 app/policies/admin_only_resource_policy.rb にポリシーを記述する。

ポリシーファイルの生成

app/policies/admin_only_resource_policy.rb:

class AdminOnlyResourcePolicy < ApplicationPolicy
  def index?
    user.role == "admin"
  end
  def show?
    user.role == "admin"
  end
  def new?
    user.role == "admin"
  end
  def edit?
    user.role == "admin"
  end
  def create?
    user.role == "admin"
  end
  def update?
    user.role == "admin"
  end
  def destroy?
    user.role == "admin"
  end
end

これで、 roleadmin のユーザー以外が見れないようになる。

A5SQL で role を user にしたり admin にしたりして試してみよう。

以上。

参考資料