Terminal Profileでcargo-competeをもっと便利に

私がAtCoderをRustで解く際、以下の記事で紹介されているcargo-competeを使っています。

https://qiita.com/qryxip/items/bff57848ac9310d27f1a

導入方法・使い方などは[README](https://github.com/qryxip/cargo-compete/blob/master/README-ja.md)などを見てください。

この記事ではcargo-competeを使ったときに発生する、ちょっとした手間を省いて便利にする方法を紹介します。

:::note warn
VSCode + AtCoderのみで使えます
:::

# 動機

## 1. コンテストを開くとき
cargo-competeで新しいコンテストを開いたとき、ディレクトリ変更したりrust-analyzerを再起動したりといった、面倒ではないが少し気になる程度の作業が必ずついてきます。

## 2. コードをテスト・提出するとき
コードをテスト・提出する`cargo compete test/submit`コマンドですが、少し長めです。
ほんの少しの手間ですが、焦っているときにtypoを連続、結果時間を食ってしまうということがあります。
私だけでしょうか。

---

コンテストの結果にはほとんど影響しないようなものですが、このようなきっかけがあったためつくり始めました。

# 使用例

## 1. コンテストを開く時
このgifのように、コンテストを開くときについてくる作業をコマンド一個で自動的にしてくれます。

![newabc_example.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111602/0f23e718-4c69-49fd-670a-eb1d3d78698a.gif)

してくれることは以下の通りです。

1. コンテストが開けるまで繰り返しリクエスト
2. ディレクトリ変更
3. rust-analyzer再起動
4. エディターでa問題を開く
5. カーソルを入力位置に合わせる
6. `s a(改行)`をクリップボードにコピー
7. 今までの出力削除
8. ブラウザでa問題を開く

## 2. コードをテスト・提出するとき

`cargo compete test` → `t`
`cargo compete submit` → `s`

それぞれ1文字になります。

---

後の章でさらに詳しく説明します。

# 手順

VSCodeの2つの設定を使います。
`terminal.integrated.defaultProfile.windows`
`terminal.integrated.profiles.windows`

ワークスペースの設定にすることで、他の場所では使えないようにしています。

:::note warn
コードを変更して動作確認をする際はシェルを再起動するのを忘れないようにしてください。
:::

なお、ファイル・フォルダの名前は自由です。

## 1. ファイル作成

### ワークスペースを作成
cargo-competeを使っているフォルダをワークスペースとして保存し、`AtCoder.code-workspace`ができます。

### プロファイルファイルを作成
次にAtCoderフォルダに`profile.ps1`をつくります。

次の画像のようなフォルダ構成になると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111602/f5b4169b-e36e-9ed6-35b9-bd13123d37e7.png)

## 2. ファイル編集

### `AtCoder.code-workspace`を編集
`settings`の中身を追記してください。
PowerShell以外のシェルを使っていたり、違うバージョンのPowerShell(powershell.exe)を使っている方は適宜変更してください。

```:AtCoder.code-workspace
{
    "folders": [
        {
            "path": "."
        }
    ],
    "settings": {
        "terminal.integrated.defaultProfile.windows": "PowerShell for AtCoder",
        "terminal.integrated.profiles.windows": {
            "PowerShell for AtCoder": {
                "path": "pwsh.exe",
                "args": [
                    "-noexit",
                    "-file",
                    "C:/AtCoder/profile.ps1"
                ]
            }
        }
    },
}

```

### `profile.ps1`を編集
下の章でそれぞれの関数の説明を読み、各自の環境に合わせてコードを変更してください。

<details><summary>コード</summary>

```:profile.ps1
function T($Problem) {
    cargo compete test $Problem
}

function S($Problem) {
    cargo compete submit $Problem
}

function NewABC($ContestNumber) {
    add-type -AssemblyName System.Windows.Forms

    $ContestName = "abc$ContestNumber"

    $strCmd = "cargo"
    $arguments = "compete new $ContestName"
    $Result = Invoke-ExternalCommand $strCmd $arguments

    while (!($Result -match "Created")) {
        Write-Host $Result.stderr
        Start-Sleep -m 500
        $Result = Invoke-ExternalCommand $strCmd $arguments
    }

    Write-Host $Result.stderr

    Set-Location "$ContestName"

    Set-Clipboard "rust-analyzer: restart server"
    [System.Windows.Forms.SendKeys]::SendWait("+^p")
    [System.Windows.Forms.SendKeys]::SendWait("^v")
    [System.Windows.Forms.SendKeys]::SendWait("{Enter}")

    Set-Clipboard "$ContestName/src/bin/a.rs"
    [System.Windows.Forms.SendKeys]::SendWait("^p")
    [System.Windows.Forms.SendKeys]::SendWait("^v")
    Start-Sleep -m 500
    [System.Windows.Forms.SendKeys]::SendWait("{Enter}")

    Start-Sleep -m 500

    # with Vim extension
    [System.Windows.Forms.SendKeys]::SendWait("{ESC}5GA")

    # without Vim extension
    [System.Windows.Forms.SendKeys]::SendWait("^g")
    [System.Windows.Forms.SendKeys]::SendWait("5")
    [System.Windows.Forms.SendKeys]::SendWait("{Enter}")
    [System.Windows.Forms.SendKeys]::SendWait("{End}")

    Set-Clipboard "cargo s a
"

    Clear-Host

    $link = "https://atcoder.jp/contests/$ContestName/tasks/$ContestName" + "_a"
    Start-Process $link
}

function Invoke-ExternalCommand([string]$commandPath, [string]$arguments) {
    try {
        # Creating process object.
        $pinfo = New-Object System.Diagnostics.Process
        # Setting process invocation parameters.
        $pinfo.StartInfo.FileName = $commandPath
        $pinfo.StartInfo.Arguments = $arguments
        $pinfo.StartInfo.CreateNoWindow = $true
        $pinfo.StartInfo.UseShellExecute = $false
        $pinfo.StartInfo.RedirectStandardOutput = $true
        $pinfo.StartInfo.RedirectStandardError = $true

        # 非同期書き込み
        # Creating string builders to store stdout and stderr.
        $oStdOutBuilder = New-Object -TypeName System.Text.StringBuilder
        $oStdErrBuilder = New-Object -TypeName System.Text.StringBuilder

        # Adding event handlers for stdout and stderr.
        $sScripBlock = {
            if (! [String]::IsNullOrEmpty($EventArgs.Data)) {
                $Event.MessageData.AppendLine($EventArgs.Data)
            }
        }
        $oStdOutEvent = Register-ObjectEvent -InputObject $pinfo `
            -Action $sScripBlock -EventName 'OutputDataReceived' `
            -MessageData $oStdOutBuilder
        $oStdErrEvent = Register-ObjectEvent -InputObject $pinfo `
            -Action $sScripBlock -EventName 'ErrorDataReceived' `
            -MessageData $oStdErrBuilder

        # Starting process.
        [Void]$pinfo.Start()
        $pinfo.BeginOutputReadLine()
        $pinfo.BeginErrorReadLine()
        [Void]$pinfo.WaitForExit()

        # Unregistering events to retrieve process output.
        Unregister-Event -SourceIdentifier $oStdOutEvent.Name
        Unregister-Event -SourceIdentifier $oStdErrEvent.Name

        $Result = New-Object -TypeName PSObject -Property ([Ordered]@{
                "ExitCode" = $pinfo.ExitCode;
                "stdout"   = $oStdOutBuilder.ToString().Trim();
                "stderr"   = $oStdErrBuilder.ToString().Trim()
            })
        return $Result
    }
    finally {
        $pinfo.Dispose()
    }
}

```
</details>

# 結果

こうすることで、AtCoderワークスペース内でターミナルを開いた際に`PowerShell for AtCoder`プロファイルが開かれ、`profile.ps1`の内容が読み込まれることで以下の関数が使えるようになります。

+ `NewABC`
+ `T`
+ `S`
+ (`Invoke-ExternalCommands`)直接使うことはない

ちなみに関数の大文字小文字は区別されないようです。

# 関数の内容

## NewABC

```:shell
newabc (コンテストの番号)
newabc 300
```

新しいコンテストを開きます。

既に書きましたが、自動で以下のことをやってくれます。

1. コンテストが開けるまで0.5秒間隔で繰り返しリクエスト
2. ディレクトリ変更
3. rust-analyzer再起動
4. エディターでa問題を開く
5. カーソルを入力位置に合わせる
6. `s a(改行)`をクリップボードにコピー
7. 今までの出力削除
8. ブラウザでa問題を開く

ARCに出る方は`NewARC`をつくっても良いかもしれません。

## 説明

### 繰り返しリクエスト
9:00ちょうどに`cargo compete new abc◯◯◯`を実行するとコンテストを開けないことがあったためこの機能をつくりました。
事前に関数を実行しておくと、テストケースがダウンロードされるまで0.5秒間隔で繰り返しコマンドを実行します。

<details><summary>該当コード</summary>

```:profile.ps1
$ContestName = "abc$ContestNumber"

$strCmd = "cargo"
$arguments = "compete new $ContestName"
$Result = Invoke-ExternalCommand $strCmd $arguments

while (!($Result -match "Created")) {
    Write-Host $Result.stderr
    Start-Sleep -m 500
    $Result = Invoke-ExternalCommand $strCmd $arguments
}
```
</details>

### ディレクトリ変更
開いたコンテストのディレクトリに移動します。

<details><summary>該当コード</summary>

```:profile.ps1
Set-Location "$ContestName"
```
</details>



### rust-analyzer再起動
新しいパッケージを開いてもそのままではrust-analyzerによる補完が効きません。
そのためrust-analyzerのサーバーを再起動してパッケージを読み込ませます。

`ctrl+shift+p`から実行しているため、ショートカットを変更している方はコードも変更してください。

<details><summary>該当コード</summary>

```:profile.ps1
Set-Clipboard "rust-analyzer: restart server"
[System.Windows.Forms.SendKeys]::SendWait("+^p")
[System.Windows.Forms.SendKeys]::SendWait("^v")
[System.Windows.Forms.SendKeys]::SendWait("{Enter}")
```

</details>


### エディターでa問題を開く
VSCodeの機能の「ファイルを名前で検索」を利用しています。
ショートカットを`ctrl+p`から変更している方はコードも変更してください。
またファイルを検索する時間とファイルを開く時間を取るため、500msのスリープを2回入れています。


<details><summary>該当コード</summary>

```:profile.ps1
Set-Clipboard "$ContestName/src/bin/a.rs"
[System.Windows.Forms.SendKeys]::SendWait("^p")
[System.Windows.Forms.SendKeys]::SendWait("^v")
Start-Sleep -m 500
[System.Windows.Forms.SendKeys]::SendWait("{Enter}")
Start-Sleep -m 500
```

</details>


### カーソルを合わせる
`compete.toml`の`[template]`の内容によります。
```:compete.toml
[template]
src = '''
use proconio::input;

fn main() {
    input! {
        
    }
}
'''
```
私はこの様になっているので、5行目の末尾にカーソルを合わせています。

Vimキーマップを使っていない場合はVSCodeの行を移動する機能を使うため、ショートカットを`ctrl+g`から変更している方はコードの方も変更してください。

<details><summary>該当コード</summary>

```:profile.ps1
# with Vim extension
[System.Windows.Forms.SendKeys]::SendWait("{ESC}5GA")

# without Vim extension
[System.Windows.Forms.SendKeys]::SendWait("^g")
[System.Windows.Forms.SendKeys]::SendWait("5")
[System.Windows.Forms.SendKeys]::SendWait("{Enter}")
[System.Windows.Forms.SendKeys]::SendWait("{End}")
```

</details>

### s a(改行)をコピー

a問題の解答を書いた後、ターミナルの上で右クリックするだけで提出が完了します。
これのおかげで1回Fastestを取れました。Firstではないですが。

<details><summary>該当コード</summary>

```:profile.ps1
Set-Clipboard "s a
"
```

</details>


### 今までの出力削除
<details><summary>該当コード</summary>

```:profile.ps1
Clear-Host
```

</details>

### ブラウザでa問題を開く

<details><summary>details</summary>

```:profile.ps1
$link = "https://atcoder.jp/contests/$ContestName/tasks/$ContestName" + "_a"
Start-Process $link
```

</details>



## t

```:shell
t (問題名)
t c
```

`cargo compete test`と同じです。

## s

```:shell
s (問題名)
s d
```

`cargo compete submit`と同じです。

# 注意点
`NewABC`で繰り返しリクエストする場合、サーバーに負担をかけそうで心配です。
私はできるだけコンテスト開催直前で実行するようにしています。

また、スリープの時間もかなりギリギリなので間違った処理をし始めたらそこを見直してみてください。

`t`と`s`、`a`から`g`をしっかりと確認してください。
違う問題のコードを提出しているかもしれません(体験談)。

# おまけ

`profile.ps1`に以下のコードを追加すると、コード補完の無効化・起動時のバージョン表示の消去ができます。
AtCoderワークスペース以外でも適用したい場合はPowerShell自体のプロファイルを編集してください。

```:profile.ps1
Set-PSReadLineOption -PredictionSource None
Clear-Host
```

# おわりに

ここまで読んで頂きありがとうございます。

もともとPowerShell Profileを使って同じことをしていたのですが、関数のスコープが気になって調べてみたところ、ワークスペースを使えば実現できそうだったためやってみました。

これくらいなら自分でも記事をかけるだろうと思い、初めての記事作成にも挑戦しました。
最初は何を書けばいいかさっぱりでしたが、先人の知恵を借りつつ書き始めてしまえば何だかんだ3日くらいで出来てしまいました。

アウトプットをすること自体苦手なので、練習として継続していければと思っています。

至らない点があればぜひご指摘ください。

# 参考文献

`Invoke-ExternalCommands`関数を使わせていただきました。

https://qiita.com/syuuu/items/81092f1f783b76f2706b

`Start-Sleep`メソッドのドキュメント

https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.utility/start-sleep

`SendWait`メソッドのドキュメント

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.sendkeys.send?view=windowsdesktop-7.0