Vernon Wee Hong KOH zrewidował ten Gist 2 weeks ago. Przejdź do rewizji
1 file changed, 8 insertions, 2 deletions
install.ps1
| @@ -92,10 +92,13 @@ foreach ($ext in $Extensions) { | |||
| 92 | 92 | Write-Host -NoNewline (" -> {0,-55} " -f $ext) | |
| 93 | 93 | $stderrFile = [System.IO.Path]::GetTempFileName() | |
| 94 | 94 | try { | |
| 95 | - | # Do not merge native stderr into PowerShell's error stream: warnings from | |
| 96 | - | # the VS Code CLI otherwise become terminating errors under Stop. | |
| 95 | + | # Windows PowerShell promotes native stderr to NativeCommandError under | |
| 96 | + | # Stop, even when redirected. Trust the CLI exit code instead. | |
| 97 | + | $previousErrorActionPreference = $ErrorActionPreference | |
| 98 | + | $ErrorActionPreference = 'Continue' | |
| 97 | 99 | $output = & $codeCmd.Path @extraArgs --install-extension $ext --force 2> $stderrFile | |
| 98 | 100 | $exitCode = $LASTEXITCODE | |
| 101 | + | $ErrorActionPreference = $previousErrorActionPreference | |
| 99 | 102 | $stderr = Get-Content -LiteralPath $stderrFile | |
| 100 | 103 | ||
| 101 | 104 | if ($exitCode -eq 0) { | |
| @@ -114,6 +117,9 @@ foreach ($ext in $Extensions) { | |||
| 114 | 117 | $failedList += $ext | |
| 115 | 118 | Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed | |
| 116 | 119 | } finally { | |
| 120 | + | if ($null -ne $previousErrorActionPreference) { | |
| 121 | + | $ErrorActionPreference = $previousErrorActionPreference | |
| 122 | + | } | |
| 117 | 123 | Remove-Item -LiteralPath $stderrFile -Force -ErrorAction SilentlyContinue | |
| 118 | 124 | } | |
| 119 | 125 | } | |
Vernon Wee Hong KOH zrewidował ten Gist 2 weeks ago. Przejdź do rewizji
1 file changed, 11 insertions, 2 deletions
install.ps1
| @@ -90,9 +90,15 @@ $failedList = @() | |||
| 90 | 90 | ||
| 91 | 91 | foreach ($ext in $Extensions) { | |
| 92 | 92 | Write-Host -NoNewline (" -> {0,-55} " -f $ext) | |
| 93 | + | $stderrFile = [System.IO.Path]::GetTempFileName() | |
| 93 | 94 | try { | |
| 94 | - | $output = & code @extraArgs --install-extension $ext --force 2>&1 | |
| 95 | - | if ($LASTEXITCODE -eq 0) { | |
| 95 | + | # Do not merge native stderr into PowerShell's error stream: warnings from | |
| 96 | + | # the VS Code CLI otherwise become terminating errors under Stop. | |
| 97 | + | $output = & $codeCmd.Path @extraArgs --install-extension $ext --force 2> $stderrFile | |
| 98 | + | $exitCode = $LASTEXITCODE | |
| 99 | + | $stderr = Get-Content -LiteralPath $stderrFile | |
| 100 | + | ||
| 101 | + | if ($exitCode -eq 0) { | |
| 96 | 102 | Write-Host "ok" -ForegroundColor Green | |
| 97 | 103 | $installed++ | |
| 98 | 104 | } else { | |
| @@ -100,12 +106,15 @@ foreach ($ext in $Extensions) { | |||
| 100 | 106 | $failed++ | |
| 101 | 107 | $failedList += $ext | |
| 102 | 108 | $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed } | |
| 109 | + | $stderr | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed } | |
| 103 | 110 | } | |
| 104 | 111 | } catch { | |
| 105 | 112 | Write-Host "FAILED" -ForegroundColor Red | |
| 106 | 113 | $failed++ | |
| 107 | 114 | $failedList += $ext | |
| 108 | 115 | Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed | |
| 116 | + | } finally { | |
| 117 | + | Remove-Item -LiteralPath $stderrFile -Force -ErrorAction SilentlyContinue | |
| 109 | 118 | } | |
| 110 | 119 | } | |
| 111 | 120 | ||
Vernon Wee Hong KOH zrewidował ten Gist 3 weeks ago. Przejdź do rewizji
5 files changed, 366 insertions, 3 deletions
README.md
| @@ -6,6 +6,8 @@ A bundle of cross-platform scripts for editor extensions and plugins, one comman | |||
| 6 | 6 | | ----------------------- | ----------------------------------- | ------------------------ | | |
| 7 | 7 | | `install.sh` | VS Code | macOS, Linux (Ubuntu) | | |
| 8 | 8 | | `install.ps1` | VS Code | Windows | | |
| 9 | + | | `settings.sh` | VS Code settings | macOS, Linux (Ubuntu) | | |
| 10 | + | | `settings.ps1` | VS Code settings | Windows | | |
| 9 | 11 | | `install-jetbrains.sh` | JetBrains IDEs + remote-dev-server | macOS, Linux, WSL2 | | |
| 10 | 12 | | `install-jetbrains.ps1` | JetBrains IDEs + Gateway Client | Windows | | |
| 11 | 13 | ||
| @@ -18,7 +20,7 @@ Replace `<GIST>` with the raw base of this Gist (everything up to and including | |||
| 18 | 20 | ### VS Code | |
| 19 | 21 | ||
| 20 | 22 | ```bash | |
| 21 | - | # 1. macOS / Ubuntu (Install to default profile from a LOCAL .vscode/extensions.json) | |
| 23 | + | # 1. macOS / Ubuntu (use local recommendations when present, otherwise install the bundled defaults) | |
| 22 | 24 | bash <(curl -sSL https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.sh) | |
| 23 | 25 | ||
| 24 | 26 | # 2. Install remote recommendation JSON to the default profile | |
| @@ -39,6 +41,16 @@ iwr -useb https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a918 | |||
| 39 | 41 | .\install.ps1 -ProfileName "WorkSetup" | |
| 40 | 42 | ``` | |
| 41 | 43 | ||
| 44 | + | When the bundled extensions are installed into the default profile, the installer also merges the curated settings described below. To apply only the settings: | |
| 45 | + | ||
| 46 | + | ```bash | |
| 47 | + | bash <(curl -sSL https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/settings.sh) | |
| 48 | + | ``` | |
| 49 | + | ||
| 50 | + | ```powershell | |
| 51 | + | iwr -useb https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/settings.ps1 | iex | |
| 52 | + | ``` | |
| 53 | + | ||
| 42 | 54 | ### JetBrains | |
| 43 | 55 | ||
| 44 | 56 | ```bash | |
| @@ -89,8 +101,16 @@ Two IDs are easy to get wrong: `IdeaVIM` (xmlId is all-caps `VIM`) and `Key Prom | |||
| 89 | 101 | - `--profile "Name"` / `-ProfileName "Name"` installs into a named profile; VS Code creates the profile if it doesn't exist. | |
| 90 | 102 | - `--force` is passed so re-runs are idempotent. | |
| 91 | 103 | - Both scripts exit non-zero if any extension fails, and print a final summary. | |
| 104 | + | - `install.sh` applies the curated settings only when it falls back to its bundled extension list and no named profile is selected. A local or remote recommendation file does not trigger settings changes. | |
| 105 | + | - `install.ps1` applies the curated settings when its bundled extensions are installed into the default profile. Named profiles do not trigger settings changes. | |
| 106 | + | ||
| 107 | + | ### Curated VS Code settings | |
| 108 | + | ||
| 109 | + | `settings.sh` and `settings.ps1` update only the managed editor, wrapping, font-family, GitHub theme, and Material Icon Theme keys. Existing unrelated settings and values are preserved. The color theme is `GitHub Dark Default`, supplied by the bundled `github.github-vscode-theme` extension. | |
| 110 | + | ||
| 111 | + | Before changing an existing `settings.json`, the scripts create a timestamped `settings.json.backup.*` file. JSONC comments and trailing commas are accepted, but the merged file is normalized to formatted JSON; the backup retains the original comments and formatting. `editor.fontSize` is intentionally not managed. | |
| 92 | 112 | ||
| 93 | - | Prereq: the `code` CLI must be on `PATH`. | |
| 113 | + | Prereqs: the extension installers require the `code` CLI on `PATH`; the Bash installer and `settings.sh` also require `jq`. The standalone settings scripts do not require `code`. | |
| 94 | 114 | ||
| 95 | 115 | - **macOS:** open VS Code → Cmd+Shift+P → "Shell Command: Install 'code' command in PATH". | |
| 96 | 116 | - **Ubuntu:** install from <https://code.visualstudio.com/> or `sudo snap install --classic code`. | |
| @@ -161,7 +181,7 @@ After Gateway installs finish, **close and re-open the Gateway Client** for it t | |||
| 161 | 181 | ||
| 162 | 182 | **`code` not on PATH.** See the VS Code installer details section above. | |
| 163 | 183 | ||
| 164 | - | **PowerShell refuses to run the script.** Each `.ps1` already sets `RemoteSigned` for the current process only. If your environment blocks that too, invoke explicitly: | |
| 184 | + | **PowerShell refuses to run an installer script.** The installer `.ps1` scripts attempt to set `RemoteSigned` for the current process only. If your environment blocks the file, invoke it explicitly: | |
| 165 | 185 | ||
| 166 | 186 | ```powershell | |
| 167 | 187 | powershell -ExecutionPolicy Bypass -File .\install.ps1 | |
install.ps1
| @@ -49,6 +49,21 @@ $Extensions = @( | |||
| 49 | 49 | 'ms-vscode-remote.vscode-remote-extensionpack', | |
| 50 | 50 | 'pkief.material-icon-theme' | |
| 51 | 51 | ) | |
| 52 | + | $SettingsUrl = 'https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/settings.ps1' | |
| 53 | + | ||
| 54 | + | function Install-DefaultSettings { | |
| 55 | + | Write-Host "" | |
| 56 | + | Write-Host 'Applying curated settings to the default VS Code profile...' -ForegroundColor Cyan | |
| 57 | + | ||
| 58 | + | $localSettingsScript = if ($PSScriptRoot) { Join-Path $PSScriptRoot 'settings.ps1' } else { $null } | |
| 59 | + | if ($localSettingsScript -and (Test-Path -LiteralPath $localSettingsScript -PathType Leaf)) { | |
| 60 | + | & $localSettingsScript | |
| 61 | + | return | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | $settingsScript = (Invoke-WebRequest -UseBasicParsing -Uri $SettingsUrl).Content | |
| 65 | + | & ([scriptblock]::Create($settingsScript)) | |
| 66 | + | } | |
| 52 | 67 | ||
| 53 | 68 | $codeCmd = Get-Command code -ErrorAction SilentlyContinue | |
| 54 | 69 | if (-not $codeCmd) { | |
| @@ -104,3 +119,7 @@ if ($failed -gt 0) { | |||
| 104 | 119 | } | |
| 105 | 120 | exit 1 | |
| 106 | 121 | } | |
| 122 | + | ||
| 123 | + | if ([string]::IsNullOrWhiteSpace($ProfileName)) { | |
| 124 | + | Install-DefaultSettings | |
| 125 | + | } | |
install.sh
| @@ -21,6 +21,7 @@ EXTENSIONS=( | |||
| 21 | 21 | ) | |
| 22 | 22 | PROFILE="" | |
| 23 | 23 | SOURCE="" | |
| 24 | + | SETTINGS_URL="https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/settings.sh" | |
| 24 | 25 | ||
| 25 | 26 | usage() { | |
| 26 | 27 | cat <<EOF | |
| @@ -145,6 +146,25 @@ install_extension() { | |||
| 145 | 146 | fi | |
| 146 | 147 | } | |
| 147 | 148 | ||
| 149 | + | apply_default_settings() { | |
| 150 | + | local script_dir settings_script | |
| 151 | + | ||
| 152 | + | script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" | |
| 153 | + | settings_script="$script_dir/settings.sh" | |
| 154 | + | ||
| 155 | + | echo | |
| 156 | + | echo "Applying curated settings to the default VS Code profile..." | |
| 157 | + | if [[ -f "$settings_script" ]]; then | |
| 158 | + | bash "$settings_script" | |
| 159 | + | else | |
| 160 | + | if ! command -v curl >/dev/null 2>&1; then | |
| 161 | + | echo "Error: 'curl' is required to download the VS Code settings script." >&2 | |
| 162 | + | return 1 | |
| 163 | + | fi | |
| 164 | + | curl -fsSL "$SETTINGS_URL" | bash | |
| 165 | + | fi | |
| 166 | + | } | |
| 167 | + | ||
| 148 | 168 | header="Installing ${#EXTENSIONS[@]} extension(s)" | |
| 149 | 169 | [[ -n "$PROFILE" ]] && header+=" into profile '$PROFILE'" | |
| 150 | 170 | echo "$header..." | |
| @@ -179,3 +199,7 @@ if (( failed > 0 )); then | |||
| 179 | 199 | done | |
| 180 | 200 | exit 1 | |
| 181 | 201 | fi | |
| 202 | + | ||
| 203 | + | if [[ "$USE_HARDCODED" == true && -z "$PROFILE" ]]; then | |
| 204 | + | apply_default_settings | |
| 205 | + | fi | |
settings.ps1(stworzono plik)
| @@ -0,0 +1,154 @@ | |||
| 1 | + | <# Merge curated defaults into the default VS Code profile settings. #> | |
| 2 | + | ||
| 3 | + | [CmdletBinding()] | |
| 4 | + | param() | |
| 5 | + | ||
| 6 | + | $ErrorActionPreference = 'Stop' | |
| 7 | + | ||
| 8 | + | function ConvertFrom-JsoncText { | |
| 9 | + | param([Parameter(Mandatory = $true)][string]$Text) | |
| 10 | + | ||
| 11 | + | $withoutComments = [System.Text.StringBuilder]::new() | |
| 12 | + | $state = 'Normal' | |
| 13 | + | $escaped = $false | |
| 14 | + | ||
| 15 | + | for ($i = 0; $i -lt $Text.Length; $i++) { | |
| 16 | + | $char = $Text[$i] | |
| 17 | + | $next = if ($i + 1 -lt $Text.Length) { $Text[$i + 1] } else { [char]0 } | |
| 18 | + | ||
| 19 | + | switch ($state) { | |
| 20 | + | 'Normal' { | |
| 21 | + | if ($char -eq '"') { | |
| 22 | + | [void]$withoutComments.Append($char) | |
| 23 | + | $state = 'String' | |
| 24 | + | } elseif ($char -eq '/' -and $next -eq '/') { | |
| 25 | + | $state = 'LineComment' | |
| 26 | + | $i++ | |
| 27 | + | } elseif ($char -eq '/' -and $next -eq '*') { | |
| 28 | + | $state = 'BlockComment' | |
| 29 | + | $i++ | |
| 30 | + | } else { | |
| 31 | + | [void]$withoutComments.Append($char) | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | 'String' { | |
| 35 | + | [void]$withoutComments.Append($char) | |
| 36 | + | if ($escaped) { | |
| 37 | + | $escaped = $false | |
| 38 | + | } elseif ($char -eq '\') { | |
| 39 | + | $escaped = $true | |
| 40 | + | } elseif ($char -eq '"') { | |
| 41 | + | $state = 'Normal' | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | 'LineComment' { | |
| 45 | + | if ($char -eq "`r" -or $char -eq "`n") { | |
| 46 | + | [void]$withoutComments.Append($char) | |
| 47 | + | $state = 'Normal' | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | 'BlockComment' { | |
| 51 | + | if ($char -eq '*' -and $next -eq '/') { | |
| 52 | + | $state = 'Normal' | |
| 53 | + | $i++ | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | if ($state -eq 'BlockComment') { | |
| 60 | + | throw 'settings.json contains an unterminated block comment.' | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | $clean = $withoutComments.ToString() | |
| 64 | + | $withoutTrailingCommas = [System.Text.StringBuilder]::new() | |
| 65 | + | $state = 'Normal' | |
| 66 | + | $escaped = $false | |
| 67 | + | ||
| 68 | + | for ($i = 0; $i -lt $clean.Length; $i++) { | |
| 69 | + | $char = $clean[$i] | |
| 70 | + | if ($state -eq 'String') { | |
| 71 | + | [void]$withoutTrailingCommas.Append($char) | |
| 72 | + | if ($escaped) { | |
| 73 | + | $escaped = $false | |
| 74 | + | } elseif ($char -eq '\') { | |
| 75 | + | $escaped = $true | |
| 76 | + | } elseif ($char -eq '"') { | |
| 77 | + | $state = 'Normal' | |
| 78 | + | } | |
| 79 | + | continue | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | if ($char -eq '"') { | |
| 83 | + | [void]$withoutTrailingCommas.Append($char) | |
| 84 | + | $state = 'String' | |
| 85 | + | continue | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | if ($char -eq ',') { | |
| 89 | + | $lookAhead = $i + 1 | |
| 90 | + | while ($lookAhead -lt $clean.Length -and [char]::IsWhiteSpace($clean[$lookAhead])) { | |
| 91 | + | $lookAhead++ | |
| 92 | + | } | |
| 93 | + | if ($lookAhead -lt $clean.Length -and ($clean[$lookAhead] -eq '}' -or $clean[$lookAhead] -eq ']')) { | |
| 94 | + | continue | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | [void]$withoutTrailingCommas.Append($char) | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | $json = $withoutTrailingCommas.ToString() | |
| 101 | + | if ([string]::IsNullOrWhiteSpace($json)) { $json = '{}' } | |
| 102 | + | return $json | ConvertFrom-Json | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | $settingsDir = if ($env:VSCODE_SETTINGS_DIR) { | |
| 106 | + | $env:VSCODE_SETTINGS_DIR | |
| 107 | + | } else { | |
| 108 | + | Join-Path $env:APPDATA 'Code\User' | |
| 109 | + | } | |
| 110 | + | $settingsFile = Join-Path $settingsDir 'settings.json' | |
| 111 | + | [void](New-Item -ItemType Directory -Path $settingsDir -Force) | |
| 112 | + | ||
| 113 | + | $backupFile = $null | |
| 114 | + | if (Test-Path -LiteralPath $settingsFile -PathType Leaf) { | |
| 115 | + | $timestamp = Get-Date -Format 'yyyyMMddHHmmssfff' | |
| 116 | + | $backupFile = "$settingsFile.backup.$timestamp" | |
| 117 | + | Copy-Item -LiteralPath $settingsFile -Destination $backupFile | |
| 118 | + | Write-Host "Backup created: $backupFile" | |
| 119 | + | $settings = ConvertFrom-JsoncText -Text ([System.IO.File]::ReadAllText($settingsFile)) | |
| 120 | + | if ($settings -isnot [System.Management.Automation.PSCustomObject]) { | |
| 121 | + | throw 'settings.json must contain a JSON object.' | |
| 122 | + | } | |
| 123 | + | } else { | |
| 124 | + | $settings = [pscustomobject]@{} | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | $managedSettings = [ordered]@{ | |
| 128 | + | 'editor.fontFamily' = "'Ubuntu Mono', 'IBM Plex Mono', 'JetBrains Mono', Hack, MonoLisa, monospace" | |
| 129 | + | 'editor.formatOnSave' = $true | |
| 130 | + | 'editor.lineHeight' = 24 | |
| 131 | + | 'editor.renderWhitespace' = 'all' | |
| 132 | + | 'editor.rulers' = @(80, 100, 120) | |
| 133 | + | 'editor.wordWrap' = 'wordWrapColumn' | |
| 134 | + | 'editor.wordWrapColumn' = 80 | |
| 135 | + | 'workbench.colorTheme' = 'GitHub Dark Default' | |
| 136 | + | 'workbench.iconTheme' = 'material-icon-theme' | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | foreach ($entry in $managedSettings.GetEnumerator()) { | |
| 140 | + | $settings | Add-Member -NotePropertyName $entry.Key -NotePropertyValue $entry.Value -Force | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | $tempFile = Join-Path $settingsDir ("settings.json.tmp.{0}" -f [guid]::NewGuid().ToString('N')) | |
| 144 | + | try { | |
| 145 | + | $json = $settings | ConvertTo-Json -Depth 100 | |
| 146 | + | [System.IO.File]::WriteAllText($tempFile, $json + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false)) | |
| 147 | + | Move-Item -LiteralPath $tempFile -Destination $settingsFile -Force | |
| 148 | + | } catch { | |
| 149 | + | Remove-Item -LiteralPath $tempFile -Force -ErrorAction SilentlyContinue | |
| 150 | + | if ($backupFile) { Write-Error "Could not merge settings. Original preserved at: $backupFile" } | |
| 151 | + | throw | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | Write-Host "VS Code settings updated: $settingsFile" -ForegroundColor Green | |
settings.sh(stworzono plik)
| @@ -0,0 +1,146 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | ||
| 3 | + | set -euo pipefail | |
| 4 | + | IFS=$'\n\t' | |
| 5 | + | ||
| 6 | + | if ! command -v jq >/dev/null 2>&1; then | |
| 7 | + | echo "Error: 'jq' is required to merge VS Code settings." >&2 | |
| 8 | + | exit 1 | |
| 9 | + | fi | |
| 10 | + | ||
| 11 | + | case "$(uname -s)" in | |
| 12 | + | Linux) | |
| 13 | + | SETTINGS_DIR="${VSCODE_SETTINGS_DIR:-$HOME/.config/Code/User}" | |
| 14 | + | ;; | |
| 15 | + | Darwin) | |
| 16 | + | SETTINGS_DIR="${VSCODE_SETTINGS_DIR:-$HOME/Library/Application Support/Code/User}" | |
| 17 | + | ;; | |
| 18 | + | *) | |
| 19 | + | echo "Error: unsupported OS '$(uname -s)'. Use settings.ps1 on Windows." >&2 | |
| 20 | + | exit 1 | |
| 21 | + | ;; | |
| 22 | + | esac | |
| 23 | + | ||
| 24 | + | mkdir -p "$SETTINGS_DIR" | |
| 25 | + | SETTINGS_FILE="$SETTINGS_DIR/settings.json" | |
| 26 | + | ||
| 27 | + | # Update the target of a dotfiles-managed symlink instead of replacing the link. | |
| 28 | + | while [[ -L "$SETTINGS_FILE" ]]; do | |
| 29 | + | LINK_TARGET="$(readlink "$SETTINGS_FILE")" | |
| 30 | + | if [[ "$LINK_TARGET" == /* ]]; then | |
| 31 | + | SETTINGS_FILE="$LINK_TARGET" | |
| 32 | + | else | |
| 33 | + | SETTINGS_FILE="$(dirname "$SETTINGS_FILE")/$LINK_TARGET" | |
| 34 | + | fi | |
| 35 | + | done | |
| 36 | + | SETTINGS_DIR="$(dirname "$SETTINGS_FILE")" | |
| 37 | + | mkdir -p "$SETTINGS_DIR" | |
| 38 | + | ||
| 39 | + | if [[ -f "$SETTINGS_FILE" ]]; then | |
| 40 | + | BACKUP_FILE="$SETTINGS_FILE.backup.$(date +%Y%m%d%H%M%S).$$" | |
| 41 | + | cp "$SETTINGS_FILE" "$BACKUP_FILE" | |
| 42 | + | echo "Backup created: $BACKUP_FILE" | |
| 43 | + | else | |
| 44 | + | BACKUP_FILE="" | |
| 45 | + | fi | |
| 46 | + | ||
| 47 | + | INPUT_FILE="$SETTINGS_FILE" | |
| 48 | + | if [[ ! -f "$INPUT_FILE" ]]; then | |
| 49 | + | INPUT_FILE="/dev/null" | |
| 50 | + | fi | |
| 51 | + | ||
| 52 | + | TEMP_FILE="$(mktemp "$SETTINGS_DIR/settings.json.tmp.XXXXXX")" | |
| 53 | + | trap 'rm -f "$TEMP_FILE"' EXIT | |
| 54 | + | ||
| 55 | + | if ! jq -Rs ' | |
| 56 | + | def strip_comments: | |
| 57 | + | reduce (explode[]) as $c ( | |
| 58 | + | {out: [], state: "normal", escaped: false}; | |
| 59 | + | if .state == "normal" then | |
| 60 | + | if $c == 34 then .out += [$c] | .state = "string" | |
| 61 | + | elif $c == 47 then .state = "slash" | |
| 62 | + | else .out += [$c] | |
| 63 | + | end | |
| 64 | + | elif .state == "slash" then | |
| 65 | + | if $c == 47 then .state = "line_comment" | |
| 66 | + | elif $c == 42 then .state = "block_comment" | |
| 67 | + | else | |
| 68 | + | .out += [47, $c] | |
| 69 | + | | .state = (if $c == 34 then "string" else "normal" end) | |
| 70 | + | end | |
| 71 | + | elif .state == "line_comment" then | |
| 72 | + | if $c == 10 or $c == 13 then .out += [$c] | .state = "normal" else . end | |
| 73 | + | elif .state == "block_comment" then | |
| 74 | + | if $c == 42 then .state = "block_star" else . end | |
| 75 | + | elif .state == "block_star" then | |
| 76 | + | if $c == 47 then .state = "normal" | |
| 77 | + | elif $c != 42 then .state = "block_comment" | |
| 78 | + | else . | |
| 79 | + | end | |
| 80 | + | else | |
| 81 | + | .out += [$c] | |
| 82 | + | | if .escaped then .escaped = false | |
| 83 | + | elif $c == 92 then .escaped = true | |
| 84 | + | elif $c == 34 then .state = "normal" | |
| 85 | + | else . | |
| 86 | + | end | |
| 87 | + | end | |
| 88 | + | ) | |
| 89 | + | | if .state == "block_comment" or .state == "block_star" then | |
| 90 | + | error("unterminated block comment") | |
| 91 | + | else | |
| 92 | + | (if .state == "slash" then .out + [47] else .out end) | |
| 93 | + | end | |
| 94 | + | | implode; | |
| 95 | + | ||
| 96 | + | def strip_trailing_commas: | |
| 97 | + | reduce (explode[]) as $c ( | |
| 98 | + | {out: [], state: "normal", escaped: false, comma: false, ws: []}; | |
| 99 | + | if .state == "string" then | |
| 100 | + | .out += [$c] | |
| 101 | + | | if .escaped then .escaped = false | |
| 102 | + | elif $c == 92 then .escaped = true | |
| 103 | + | elif $c == 34 then .state = "normal" | |
| 104 | + | else . | |
| 105 | + | end | |
| 106 | + | elif .comma and ($c == 9 or $c == 10 or $c == 13 or $c == 32) then | |
| 107 | + | .ws += [$c] | |
| 108 | + | elif .comma and ($c == 93 or $c == 125) then | |
| 109 | + | .out += [$c] | .comma = false | .ws = [] | |
| 110 | + | else | |
| 111 | + | (if .comma then .out += [44] + .ws | .comma = false | .ws = [] else . end) | |
| 112 | + | | if $c == 44 then .comma = true | |
| 113 | + | else | |
| 114 | + | .out += [$c] | |
| 115 | + | | if $c == 34 then .state = "string" else . end | |
| 116 | + | end | |
| 117 | + | end | |
| 118 | + | ) | |
| 119 | + | | (if .comma then .out + [44] + .ws else .out end) | |
| 120 | + | | implode; | |
| 121 | + | ||
| 122 | + | (if test("^[[:space:]]*$") then "{}" else . end) | |
| 123 | + | | strip_comments | |
| 124 | + | | strip_trailing_commas | |
| 125 | + | | fromjson | |
| 126 | + | | if type != "object" then error("settings.json must contain a JSON object") else . end | |
| 127 | + | | . + { | |
| 128 | + | "editor.fontFamily": "\u0027Ubuntu Mono\u0027, \u0027IBM Plex Mono\u0027, \u0027JetBrains Mono\u0027, Hack, MonoLisa, monospace", | |
| 129 | + | "editor.formatOnSave": true, | |
| 130 | + | "editor.lineHeight": 24, | |
| 131 | + | "editor.renderWhitespace": "all", | |
| 132 | + | "editor.rulers": [80, 100, 120], | |
| 133 | + | "editor.wordWrap": "wordWrapColumn", | |
| 134 | + | "editor.wordWrapColumn": 80, | |
| 135 | + | "workbench.colorTheme": "GitHub Dark Default", | |
| 136 | + | "workbench.iconTheme": "material-icon-theme" | |
| 137 | + | } | |
| 138 | + | ' "$INPUT_FILE" >"$TEMP_FILE"; then | |
| 139 | + | echo "Error: could not parse and merge $SETTINGS_FILE." >&2 | |
| 140 | + | [[ -n "$BACKUP_FILE" ]] && echo "Original preserved at: $BACKUP_FILE" >&2 | |
| 141 | + | exit 1 | |
| 142 | + | fi | |
| 143 | + | ||
| 144 | + | mv "$TEMP_FILE" "$SETTINGS_FILE" | |
| 145 | + | trap - EXIT | |
| 146 | + | echo "VS Code settings updated: $SETTINGS_FILE" | |
Vernon Wee Hong KOH zrewidował ten Gist 1 month ago. Przejdź do rewizji
2 files changed, 7 insertions, 2 deletions
install-jetbrains.ps1
| @@ -380,7 +380,10 @@ $installedPluginInstances = @() | |||
| 380 | 380 | if ($action -eq 'Install') { | |
| 381 | 381 | $pluginChoices = $PluginCatalog | |
| 382 | 382 | } else { | |
| 383 | - | $installedPluginInstances = @($selectedIdes | ForEach-Object { Get-InstalledPlugins -Ide $_ }) | |
| 383 | + | $installedPluginInstances = @($selectedIdes | | |
| 384 | + | ForEach-Object { Get-InstalledPlugins -Ide $_ } | | |
| 385 | + | Group-Object Path | | |
| 386 | + | ForEach-Object { $_.Group[0] }) | |
| 384 | 387 | $pluginChoices = @($installedPluginInstances | | |
| 385 | 388 | Group-Object Id | | |
| 386 | 389 | ForEach-Object { [pscustomobject]@{ Id = $_.Name; Label = $_.Group[0].Label } } | | |
install-jetbrains.sh
| @@ -530,7 +530,7 @@ else | |||
| 530 | 530 | exit 1 | |
| 531 | 531 | fi | |
| 532 | 532 | ||
| 533 | - | declare -A seen_plugin_ids=() | |
| 533 | + | declare -A seen_plugin_ids=() seen_plugin_paths=() | |
| 534 | 534 | for ide_label in "${SELECTED_IDES[@]}"; do | |
| 535 | 535 | itype="${label_type[$ide_label]}" | |
| 536 | 536 | launcher="${label_launcher[$ide_label]}" | |
| @@ -543,6 +543,8 @@ else | |||
| 543 | 543 | while IFS= read -r -d '' item; do | |
| 544 | 544 | metadata="$(plugin_metadata "$item" || true)" | |
| 545 | 545 | [[ -n "$metadata" ]] || continue | |
| 546 | + | [[ -z "${seen_plugin_paths[$item]+x}" ]] || continue | |
| 547 | + | seen_plugin_paths["$item"]=1 | |
| 546 | 548 | IFS='|' read -r pid plabel <<<"$metadata" | |
| 547 | 549 | INSTALLED_PLUGIN_RECORDS+=("${pid}|${plabel}|${item}|${plugin_root}|${ide_label}") | |
| 548 | 550 | if [[ -z "${seen_plugin_ids[$pid]+x}" ]]; then | |
Vernon Wee Hong KOH zrewidował ten Gist 1 month ago. Przejdź do rewizji
3 files changed, 500 insertions, 34 deletions
README.md
| @@ -1,6 +1,6 @@ | |||
| 1 | - | # VS Code + JetBrains Extension Installers | |
| 1 | + | # VS Code + JetBrains Extension Managers | |
| 2 | 2 | ||
| 3 | - | A bundle of cross-platform scripts that install a curated set of editor extensions / plugins, one command per platform. | |
| 3 | + | A bundle of cross-platform scripts for editor extensions and plugins, one command per platform. The JetBrains scripts install and uninstall plugins; the VS Code scripts install extensions. | |
| 4 | 4 | ||
| 5 | 5 | | File | Editor | Platforms | | |
| 6 | 6 | | ----------------------- | ----------------------------------- | ------------------------ | | |
| @@ -42,12 +42,12 @@ iwr -useb https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a918 | |||
| 42 | 42 | ### JetBrains | |
| 43 | 43 | ||
| 44 | 44 | ```bash | |
| 45 | - | # macOS / Ubuntu / WSL2 — interactive picker for IDEs and plugins | |
| 45 | + | # macOS / Ubuntu / WSL2 — IDE, action, then plugin picker | |
| 46 | 46 | bash <(curl -sSL https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install-jetbrains.sh) | |
| 47 | 47 | ``` | |
| 48 | 48 | ||
| 49 | 49 | ```powershell | |
| 50 | - | # Windows — interactive picker | |
| 50 | + | # Windows — IDE, action, then plugin picker | |
| 51 | 51 | iwr -useb https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install-jetbrains.ps1 | iex | |
| 52 | 52 | ``` | |
| 53 | 53 | ||
| @@ -104,8 +104,12 @@ Both JetBrains scripts: | |||
| 104 | 104 | ||
| 105 | 105 | 1. Detect the host OS. | |
| 106 | 106 | 2. Scan the system for installed JetBrains IDEs and (where applicable) Gateway / remote-dev targets. | |
| 107 | - | 3. Show an interactive multi-select picker for IDEs and another for plugins. | |
| 108 | - | 4. Run each IDE's `installPlugins` against every selected plugin. | |
| 107 | + | 3. Show the IDE picker first. | |
| 108 | + | 4. Ask whether to install or uninstall. | |
| 109 | + | 5. For install, show the curated catalog; for uninstall, show the user-installed plugins actually found in the selected IDEs. | |
| 110 | + | 6. Install the selected marketplace IDs or, after showing exact paths and asking for confirmation, remove the selected plugin files. | |
| 111 | + | ||
| 112 | + | JetBrains AI Assistant is identified by its marketplace ID, `com.intellij.ml.llm`, so uninstall works even though its on-disk directory is normally named `ml-llm`. | |
| 109 | 113 | ||
| 110 | 114 | ### What gets scanned | |
| 111 | 115 | ||
| @@ -134,9 +138,15 @@ WSL2 detection works by grepping `microsoft` in `/proc/version`. The Windows use | |||
| 134 | 138 | ||
| 135 | 139 | ### Picker | |
| 136 | 140 | ||
| 137 | - | - **Bash:** uses `fzf --multi` if `fzf` is on `PATH` (TAB to toggle, Enter to confirm); otherwise prints a numbered list and accepts comma-separated indexes (`1,3,5`) or `a` for all. | |
| 141 | + | - **Bash:** uses `fzf --multi` if `fzf` is on `PATH` (TAB to toggle, Enter to confirm); otherwise prints a numbered list and accepts space-separated indexes (`1 3 5`), exclusions (`!2 !4`), or `a` for all. | |
| 138 | 142 | - **PowerShell:** uses `Out-GridView -OutputMode Multiple` when available; otherwise the same numbered/comma-input fallback. | |
| 139 | 143 | ||
| 144 | + | ### Uninstall behavior | |
| 145 | + | ||
| 146 | + | JetBrains launchers provide `installPlugins` but no matching uninstall command. The scripts therefore resolve the selected IDE's `dataDirectoryName` from `product-info.json`, inspect each user-installed plugin's `META-INF/plugin.xml`, and present those plugin names and IDs for selection. Before removal, the script prints every resolved IDE and path and requires confirmation. | |
| 147 | + | ||
| 148 | + | Only user-installed plugins are offered. Bundled plugins cannot be removed this way and must be disabled in the IDE. Restart the selected IDEs after uninstalling. | |
| 149 | + | ||
| 140 | 150 | ### Gateway Client path (Windows) | |
| 141 | 151 | ||
| 142 | 152 | When the script detects `%LOCALAPPDATA%\JetBrains\JetBrainsClient*` and you select it from the picker, the install logic switches: the bundled Gateway client has no `installPlugins` CLI, so the script reads the client's `build.txt` and downloads each plugin directly from `plugins.jetbrains.com`, unpacking it into the client's `plugins\` folder. | |
| @@ -159,7 +169,7 @@ powershell -ExecutionPolicy Bypass -File .\install.ps1 | |||
| 159 | 169 | ||
| 160 | 170 | **`installPlugins` reports a plugin is already installed.** Harmless — it's a no-op. Both scripts treat it as success. | |
| 161 | 171 | ||
| 162 | - | **"Only one instance of IDEA can be run at a time."** The IDE is currently open and holds the config-dir lock. Both JetBrains installers run a preflight check before installing: if a target IDE looks running, they prompt you to close it and press Enter to retry. If a process slips past the preflight, the install loop short-circuits with the same hint instead of repeating the error per plugin. | |
| 172 | + | **"Only one instance of IDEA can be run at a time."** The IDE is currently open and holds the config-dir lock. Both JetBrains scripts run a preflight check before changing plugins: if a target IDE looks running, they prompt you to close it and press Enter to retry. If a process slips past the preflight, the install loop short-circuits with the same hint instead of repeating the error per plugin. | |
| 163 | 173 | ||
| 164 | 174 | **No JetBrains IDEs detected.** Install one via the [JetBrains Toolbox](https://www.jetbrains.com/toolbox-app/) (which sets up the standard paths the script scans), or install standalone — both layouts are supported. | |
| 165 | 175 | ||
| @@ -173,4 +183,4 @@ powershell -ExecutionPolicy Bypass -File .\install.ps1 | |||
| 173 | 183 | ||
| 174 | 184 | - Scripts are idempotent — already-installed extensions/plugins are skipped or upgraded in place. | |
| 175 | 185 | - Exit code is non-zero if any extension/plugin failed. | |
| 176 | - | - The JetBrains scripts ask every time; there's no flag to skip the picker by design. | |
| 186 | + | - The JetBrains scripts ask for IDE, action, and plugin choices every time; there's no flag to skip the picker by design. | |
install-jetbrains.ps1
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | <# | |
| 2 | 2 | .SYNOPSIS | |
| 3 | - | Install JetBrains plugins into locally installed IDEs (and into the | |
| 4 | - | JetBrains Gateway Client) on Windows. | |
| 3 | + | Install or uninstall JetBrains plugins in locally installed IDEs (and in | |
| 4 | + | the JetBrains Gateway Client) on Windows. | |
| 5 | 5 | ||
| 6 | 6 | .DESCRIPTION | |
| 7 | 7 | Detects every JetBrains IDE under %LOCALAPPDATA%\Programs\* (standalone | |
| @@ -9,8 +9,10 @@ | |||
| 9 | 9 | %LOCALAPPDATA%\JetBrains\Toolbox\scripts\*.cmd, and the most-recent | |
| 10 | 10 | JetBrains Gateway Client under %LOCALAPPDATA%\JetBrains\JetBrainsClient*. | |
| 11 | 11 | ||
| 12 | - | You multi-select IDE(s) and plugin(s) interactively. Standard IDEs get | |
| 13 | - | "installPlugins <id>..." invoked once with all selected plugins. The | |
| 12 | + | You select IDE(s), choose install or uninstall, and then select plugin(s). | |
| 13 | + | Standard IDEs get "installPlugins <id>..." invoked once with all selected | |
| 14 | + | plugins. Uninstall reads plugin IDs from the selected IDEs' user plugin | |
| 15 | + | directories and removes only the entries selected by the user. The | |
| 14 | 16 | Gateway Client path is special: the bundled client has no installPlugins | |
| 15 | 17 | CLI, so plugins are downloaded directly from plugins.jetbrains.com and | |
| 16 | 18 | unpacked into the client's plugins folder (mirrors the user's | |
| @@ -37,6 +39,11 @@ try { | |||
| 37 | 39 | ||
| 38 | 40 | $ErrorActionPreference = 'Stop' | |
| 39 | 41 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | |
| 42 | + | try { | |
| 43 | + | Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop | |
| 44 | + | } catch { | |
| 45 | + | Write-Verbose ("Could not preload ZIP support: {0}" -f $_.Exception.Message) | |
| 46 | + | } | |
| 40 | 47 | ||
| 41 | 48 | # ------------------------------------------------------------------ | |
| 42 | 49 | # Canonical plugin catalog. Each entry: @{ Id; Label } | |
| @@ -197,6 +204,168 @@ function Select-Multi { | |||
| 197 | 204 | } | |
| 198 | 205 | } | |
| 199 | 206 | ||
| 207 | + | function Select-Action { | |
| 208 | + | while ($true) { | |
| 209 | + | Write-Host "" | |
| 210 | + | Write-Host "What do you want to do?" -ForegroundColor Cyan | |
| 211 | + | Write-Host " 1) Install plugins" | |
| 212 | + | Write-Host " 2) Uninstall plugins" | |
| 213 | + | $choice = Read-Host "Action (1 or 2)" | |
| 214 | + | switch ($choice.Trim().ToLower()) { | |
| 215 | + | { $_ -in @('1', 'i', 'install') } { return 'Install' } | |
| 216 | + | { $_ -in @('2', 'u', 'uninstall') } { return 'Uninstall' } | |
| 217 | + | default { Write-Host " invalid input; enter 1 for install or 2 for uninstall." -ForegroundColor Yellow } | |
| 218 | + | } | |
| 219 | + | } | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | # Find product-info.json for the selected launcher and return the IDE's | |
| 223 | + | # dataDirectoryName. This prevents uninstall from touching unrelated profiles. | |
| 224 | + | function Get-ProductDataName { | |
| 225 | + | param([Parameter(Mandatory)] [psobject]$Ide) | |
| 226 | + | ||
| 227 | + | if ($Ide.Type -eq 'Gateway') { return $null } | |
| 228 | + | $probe = $Ide.Launcher | |
| 229 | + | ||
| 230 | + | # Toolbox .cmd shims normally contain the real launcher path. | |
| 231 | + | if ($Ide.Type -eq 'WinCmd' -and (Test-Path -LiteralPath $probe)) { | |
| 232 | + | try { | |
| 233 | + | $cmdText = Get-Content -LiteralPath $probe -Raw -ErrorAction Stop | |
| 234 | + | $match = [regex]::Match($cmdText, '"([^"\r\n]+\.exe)"', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) | |
| 235 | + | if ($match.Success) { | |
| 236 | + | $expanded = [Environment]::ExpandEnvironmentVariables($match.Groups[1].Value) | |
| 237 | + | if (Test-Path -LiteralPath $expanded) { $probe = $expanded } | |
| 238 | + | } | |
| 239 | + | } catch { | |
| 240 | + | Write-Verbose ("Could not resolve Toolbox shim {0}: {1}" -f $probe, $_.Exception.Message) | |
| 241 | + | } | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | try { | |
| 245 | + | $item = Get-Item -LiteralPath $probe -ErrorAction Stop | |
| 246 | + | $dir = if ($item.PSIsContainer) { $item.FullName } else { $item.Directory.FullName } | |
| 247 | + | } catch { return $null } | |
| 248 | + | ||
| 249 | + | while ($dir) { | |
| 250 | + | foreach ($relative in @('product-info.json', 'Resources\product-info.json')) { | |
| 251 | + | $infoPath = Join-Path $dir $relative | |
| 252 | + | if (-not (Test-Path -LiteralPath $infoPath)) { continue } | |
| 253 | + | try { | |
| 254 | + | $info = Get-Content -LiteralPath $infoPath -Raw -ErrorAction Stop | ConvertFrom-Json | |
| 255 | + | if ($info.dataDirectoryName) { return [string]$info.dataDirectoryName } | |
| 256 | + | } catch { | |
| 257 | + | Write-Verbose ("Could not read {0}: {1}" -f $infoPath, $_.Exception.Message) | |
| 258 | + | } | |
| 259 | + | } | |
| 260 | + | $parent = [System.IO.Directory]::GetParent($dir) | |
| 261 | + | if (-not $parent) { break } | |
| 262 | + | $dir = $parent.FullName | |
| 263 | + | } | |
| 264 | + | return $null | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | function Get-PluginRoot { | |
| 268 | + | param([Parameter(Mandatory)] [psobject]$Ide) | |
| 269 | + | if ($Ide.Type -eq 'Gateway') { return (Join-Path $Ide.Client 'plugins') } | |
| 270 | + | ||
| 271 | + | $dataName = Get-ProductDataName -Ide $Ide | |
| 272 | + | if (-not $dataName) { return $null } | |
| 273 | + | return (Join-Path (Join-Path $env:APPDATA 'JetBrains') (Join-Path $dataName 'plugins')) | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | function ConvertFrom-PluginXml { | |
| 277 | + | param([Parameter(Mandatory)] [string]$Text) | |
| 278 | + | try { | |
| 279 | + | $settings = New-Object System.Xml.XmlReaderSettings | |
| 280 | + | $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit | |
| 281 | + | $stringReader = New-Object System.IO.StringReader($Text) | |
| 282 | + | $reader = [System.Xml.XmlReader]::Create($stringReader, $settings) | |
| 283 | + | $doc = New-Object System.Xml.XmlDocument | |
| 284 | + | $doc.XmlResolver = $null | |
| 285 | + | $doc.Load($reader) | |
| 286 | + | $reader.Dispose() | |
| 287 | + | $stringReader.Dispose() | |
| 288 | + | ||
| 289 | + | $idNode = $doc.SelectSingleNode('/idea-plugin/id') | |
| 290 | + | $nameNode = $doc.SelectSingleNode('/idea-plugin/name') | |
| 291 | + | $id = if ($idNode) { $idNode.InnerText.Trim() } elseif ($nameNode) { $nameNode.InnerText.Trim() } else { $null } | |
| 292 | + | $name = if ($nameNode) { $nameNode.InnerText.Trim() } else { $id } | |
| 293 | + | if ($id) { return [pscustomobject]@{ Id = $id; Label = $name } } | |
| 294 | + | } catch { | |
| 295 | + | Write-Verbose ("Could not parse plugin.xml: {0}" -f $_.Exception.Message) | |
| 296 | + | } | |
| 297 | + | return $null | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | function Get-ArchivePluginDescriptor { | |
| 301 | + | param([Parameter(Mandatory)] [string]$Path) | |
| 302 | + | $zip = $null | |
| 303 | + | try { | |
| 304 | + | $zip = [System.IO.Compression.ZipFile]::OpenRead($Path) | |
| 305 | + | $entry = $zip.Entries | Where-Object { $_.FullName -eq 'META-INF/plugin.xml' } | Select-Object -First 1 | |
| 306 | + | if (-not $entry) { return $null } | |
| 307 | + | $stream = $entry.Open() | |
| 308 | + | $reader = New-Object System.IO.StreamReader($stream) | |
| 309 | + | $text = $reader.ReadToEnd() | |
| 310 | + | $reader.Dispose() | |
| 311 | + | $stream.Dispose() | |
| 312 | + | return (ConvertFrom-PluginXml -Text $text) | |
| 313 | + | } catch { | |
| 314 | + | Write-Verbose ("Could not inspect plugin archive {0}: {1}" -f $Path, $_.Exception.Message) | |
| 315 | + | return $null | |
| 316 | + | } finally { | |
| 317 | + | if ($zip) { $zip.Dispose() } | |
| 318 | + | } | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | function Get-PluginDescriptor { | |
| 322 | + | param([Parameter(Mandatory)] [System.IO.FileSystemInfo]$Item) | |
| 323 | + | ||
| 324 | + | if (-not $Item.PSIsContainer) { | |
| 325 | + | return (Get-ArchivePluginDescriptor -Path $Item.FullName) | |
| 326 | + | } | |
| 327 | + | ||
| 328 | + | $looseXml = Join-Path $Item.FullName 'META-INF\plugin.xml' | |
| 329 | + | if (Test-Path -LiteralPath $looseXml) { | |
| 330 | + | try { | |
| 331 | + | return (ConvertFrom-PluginXml -Text (Get-Content -LiteralPath $looseXml -Raw -ErrorAction Stop)) | |
| 332 | + | } catch { | |
| 333 | + | Write-Verbose ("Could not inspect {0}: {1}" -f $looseXml, $_.Exception.Message) | |
| 334 | + | } | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | $archives = Get-ChildItem -LiteralPath $Item.FullName -Recurse -File -ErrorAction SilentlyContinue | | |
| 338 | + | Where-Object { $_.Extension -in @('.jar', '.zip') } | |
| 339 | + | foreach ($archive in $archives) { | |
| 340 | + | $descriptor = Get-ArchivePluginDescriptor -Path $archive.FullName | |
| 341 | + | if ($descriptor) { return $descriptor } | |
| 342 | + | } | |
| 343 | + | return $null | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | function Get-InstalledPlugins { | |
| 347 | + | param([Parameter(Mandatory)] [psobject]$Ide) | |
| 348 | + | $root = Get-PluginRoot -Ide $Ide | |
| 349 | + | if (-not $root) { | |
| 350 | + | Write-Host ("Could not resolve the plugin directory for: {0}" -f $Ide.Label) -ForegroundColor Yellow | |
| 351 | + | return | |
| 352 | + | } | |
| 353 | + | if (-not (Test-Path -LiteralPath $root)) { return } | |
| 354 | + | ||
| 355 | + | Get-ChildItem -LiteralPath $root -Force -ErrorAction SilentlyContinue | ForEach-Object { | |
| 356 | + | $descriptor = Get-PluginDescriptor -Item $_ | |
| 357 | + | if ($descriptor) { | |
| 358 | + | [pscustomobject]@{ | |
| 359 | + | Id = $descriptor.Id | |
| 360 | + | Label = $descriptor.Label | |
| 361 | + | Path = $_.FullName | |
| 362 | + | PluginRoot = $root | |
| 363 | + | TargetLabel = $Ide.Label | |
| 364 | + | } | |
| 365 | + | } | |
| 366 | + | } | |
| 367 | + | } | |
| 368 | + | ||
| 200 | 369 | Write-Host ("Detected {0} JetBrains target(s)." -f $Candidates.Count) -ForegroundColor Green | |
| 201 | 370 | ||
| 202 | 371 | $selectedIdes = Select-Multi -Title "Pick IDE(s)" -Items $Candidates -Property 'Label' | |
| @@ -205,7 +374,26 @@ if ($selectedIdes.Count -eq 0) { | |||
| 205 | 374 | exit 1 | |
| 206 | 375 | } | |
| 207 | 376 | ||
| 208 | - | $selectedPlugins = Select-Multi -Title "Pick plugin(s)" -Items $PluginCatalog -Property 'Label' | |
| 377 | + | $action = Select-Action | |
| 378 | + | $installedPluginInstances = @() | |
| 379 | + | ||
| 380 | + | if ($action -eq 'Install') { | |
| 381 | + | $pluginChoices = $PluginCatalog | |
| 382 | + | } else { | |
| 383 | + | $installedPluginInstances = @($selectedIdes | ForEach-Object { Get-InstalledPlugins -Ide $_ }) | |
| 384 | + | $pluginChoices = @($installedPluginInstances | | |
| 385 | + | Group-Object Id | | |
| 386 | + | ForEach-Object { [pscustomobject]@{ Id = $_.Name; Label = $_.Group[0].Label } } | | |
| 387 | + | Sort-Object Label) | |
| 388 | + | ||
| 389 | + | if ($pluginChoices.Count -eq 0) { | |
| 390 | + | Write-Host "No removable user-installed plugins were found for the selected IDE(s)." -ForegroundColor Yellow | |
| 391 | + | Write-Host "Bundled plugins cannot be uninstalled; disable them in the IDE instead." -ForegroundColor Yellow | |
| 392 | + | exit 0 | |
| 393 | + | } | |
| 394 | + | } | |
| 395 | + | ||
| 396 | + | $selectedPlugins = Select-Multi -Title ("Pick plugin(s) to {0}" -f $action.ToLower()) -Items $pluginChoices -Property 'Label' | |
| 209 | 397 | if ($selectedPlugins.Count -eq 0) { | |
| 210 | 398 | Write-Host "No plugins selected; exiting." -ForegroundColor Red | |
| 211 | 399 | exit 1 | |
| @@ -213,13 +401,20 @@ if ($selectedPlugins.Count -eq 0) { | |||
| 213 | 401 | ||
| 214 | 402 | # ------------------------------------------------------------------ | |
| 215 | 403 | # Running-IDE preflight | |
| 404 | + | # Installing or deleting plugin files requires the selected IDE to be closed. | |
| 216 | 405 | # installPlugins refuses to run while the IDE GUI holds the config-dir lock | |
| 217 | 406 | # ("Only one instance of IDEA can be run at a time."). Best-effort detection | |
| 218 | 407 | # via Get-Process. Gateway Client type doesn't need this (no CLI invoked). | |
| 219 | 408 | # ------------------------------------------------------------------ | |
| 220 | 409 | function Test-IdeRunning { | |
| 221 | 410 | param([Parameter(Mandatory)] [psobject]$Ide) | |
| 222 | - | if ($Ide.Type -eq 'Gateway') { return $false } | |
| 411 | + | if ($Ide.Type -eq 'Gateway') { | |
| 412 | + | if ($action -eq 'Install') { return $false } | |
| 413 | + | foreach ($name in @('jetbrains_client64', 'jetbrains_client', 'JetBrainsClient')) { | |
| 414 | + | if (Get-Process -Name $name -ErrorAction SilentlyContinue) { return $true } | |
| 415 | + | } | |
| 416 | + | return $false | |
| 417 | + | } | |
| 223 | 418 | ||
| 224 | 419 | # For standalone IDE launchers the process name is the exe basename. | |
| 225 | 420 | # For Toolbox .cmd shims, the spawned process is e.g. idea64 -> use prefix. | |
| @@ -238,15 +433,65 @@ while ($true) { | |||
| 238 | 433 | if ($running.Count -eq 0) { break } | |
| 239 | 434 | ||
| 240 | 435 | Write-Host "" | |
| 241 | - | Write-Host "The following IDE(s) appear to be running — installPlugins needs them closed:" -ForegroundColor Yellow | |
| 436 | + | Write-Host "The following IDE(s) appear to be running — plugin changes need them closed:" -ForegroundColor Yellow | |
| 242 | 437 | foreach ($r in $running) { Write-Host (" - {0}" -f $r.Label) -ForegroundColor Yellow } | |
| 243 | 438 | Write-Host "" | |
| 244 | 439 | Read-Host "Close them, then press Enter to retry (Ctrl+C to abort)" | Out-Null | |
| 245 | 440 | } | |
| 246 | 441 | ||
| 247 | 442 | # ------------------------------------------------------------------ | |
| 248 | - | # Install loop | |
| 443 | + | # Uninstall or install | |
| 249 | 444 | # ------------------------------------------------------------------ | |
| 445 | + | if ($action -eq 'Uninstall') { | |
| 446 | + | $selectedIds = @($selectedPlugins | ForEach-Object { $_.Id }) | |
| 447 | + | $toRemove = @($installedPluginInstances | Where-Object { $_.Id -in $selectedIds } | Where-Object { | |
| 448 | + | $parent = [System.IO.Directory]::GetParent($_.Path) | |
| 449 | + | $parent -and ($parent.FullName.TrimEnd('\') -eq $_.PluginRoot.TrimEnd('\')) | |
| 450 | + | }) | |
| 451 | + | ||
| 452 | + | if ($toRemove.Count -eq 0) { | |
| 453 | + | Write-Host "None of the selected plugins are installed in the selected IDE(s)." -ForegroundColor Yellow | |
| 454 | + | exit 0 | |
| 455 | + | } | |
| 456 | + | ||
| 457 | + | Write-Host "" | |
| 458 | + | Write-Host "The following plugin installations will be removed:" -ForegroundColor Yellow | |
| 459 | + | foreach ($plugin in $toRemove) { | |
| 460 | + | Write-Host (" - {0} ({1})" -f $plugin.Label, $plugin.Id) | |
| 461 | + | Write-Host (" IDE: {0}" -f $plugin.TargetLabel) | |
| 462 | + | Write-Host (" Path: {0}" -f $plugin.Path) | |
| 463 | + | } | |
| 464 | + | $confirm = Read-Host "Continue? [y/N]" | |
| 465 | + | if ($confirm -notmatch '^[Yy]$') { | |
| 466 | + | Write-Host "Uninstall cancelled." | |
| 467 | + | exit 0 | |
| 468 | + | } | |
| 469 | + | ||
| 470 | + | $removed = 0 | |
| 471 | + | $failed = 0 | |
| 472 | + | $failedList = @() | |
| 473 | + | foreach ($plugin in $toRemove) { | |
| 474 | + | Write-Host -NoNewline ("Removing {0} from {1}... " -f $plugin.Label, $plugin.TargetLabel) | |
| 475 | + | try { | |
| 476 | + | Remove-Item -LiteralPath $plugin.Path -Recurse -Force -ErrorAction Stop | |
| 477 | + | Write-Host "ok" -ForegroundColor Green | |
| 478 | + | $removed++ | |
| 479 | + | } catch { | |
| 480 | + | Write-Host "FAILED" -ForegroundColor Red | |
| 481 | + | Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed | |
| 482 | + | $failed++ | |
| 483 | + | $failedList += ("{0} :: {1}" -f $plugin.TargetLabel, $plugin.Id) | |
| 484 | + | } | |
| 485 | + | } | |
| 486 | + | ||
| 487 | + | Write-Host ("Done. Removed: {0}, Failed: {1}. Restart the IDE(s) to finish." -f $removed, $failed) | |
| 488 | + | if ($failed -gt 0) { | |
| 489 | + | foreach ($entry in $failedList) { Write-Host (" - {0}" -f $entry) -ForegroundColor Red } | |
| 490 | + | exit 1 | |
| 491 | + | } | |
| 492 | + | exit 0 | |
| 493 | + | } | |
| 494 | + | ||
| 250 | 495 | $installed = 0 | |
| 251 | 496 | $failed = 0 | |
| 252 | 497 | $failedList = @() | |
install-jetbrains.sh
| @@ -1,11 +1,11 @@ | |||
| 1 | 1 | #!/usr/bin/env bash | |
| 2 | 2 | # | |
| 3 | - | # JetBrains IDE plugin installer for macOS, Linux (Ubuntu Desktop) and WSL2. | |
| 3 | + | # JetBrains IDE plugin manager for macOS, Linux (Ubuntu Desktop) and WSL2. | |
| 4 | 4 | # | |
| 5 | 5 | # Detects every locally installed JetBrains IDE (Toolbox + standalone) and, | |
| 6 | 6 | # on Linux, any JetBrains Gateway remote-dev-server.sh distributions. Lets | |
| 7 | - | # you multi-select IDE(s) and plugin(s) interactively, then installs each | |
| 8 | - | # plugin into each selected IDE. | |
| 7 | + | # you multi-select IDE(s), choose install or uninstall, and then select the | |
| 8 | + | # plugins to manage. | |
| 9 | 9 | # | |
| 10 | 10 | # Picker UX: | |
| 11 | 11 | # - Uses fzf with --multi when fzf is on PATH (TAB to toggle, Enter to confirm). | |
| @@ -54,14 +54,15 @@ usage() { | |||
| 54 | 54 | cat <<EOF | |
| 55 | 55 | Usage: $(basename "$0") | |
| 56 | 56 | ||
| 57 | - | Interactive JetBrains IDE plugin installer for macOS, Linux and WSL2. | |
| 57 | + | Interactive JetBrains IDE plugin manager for macOS, Linux and WSL2. | |
| 58 | 58 | ||
| 59 | 59 | The script: | |
| 60 | 60 | 1. Detects your OS (macOS / Linux / WSL2) | |
| 61 | 61 | 2. Scans for installed JetBrains IDEs (Toolbox + standalone) and any | |
| 62 | 62 | local remote-dev-server.sh distributions | |
| 63 | - | 3. Lets you multi-select IDE(s) and plugin(s) | |
| 64 | - | 4. Runs each launcher's "installPlugins" command for every selected plugin | |
| 63 | + | 3. Lets you choose whether to install or uninstall plugins | |
| 64 | + | 4. Installs catalog plugins, or lists the selected IDEs' installed plugins | |
| 65 | + | and lets you choose which ones to remove | |
| 65 | 66 | ||
| 66 | 67 | Use \`brew install fzf\` (or your distro's package manager) for a nicer picker. | |
| 67 | 68 | EOF | |
| @@ -366,6 +367,119 @@ pick_multi() { | |||
| 366 | 367 | done | |
| 367 | 368 | } | |
| 368 | 369 | ||
| 370 | + | pick_action() { | |
| 371 | + | local input | |
| 372 | + | while :; do | |
| 373 | + | { | |
| 374 | + | echo | |
| 375 | + | echo "What do you want to do?" | |
| 376 | + | echo " 1) Install plugins" | |
| 377 | + | echo " 2) Uninstall plugins" | |
| 378 | + | printf 'Action (1 or 2): ' | |
| 379 | + | } >&2 | |
| 380 | + | if ! IFS= read -r input </dev/tty; then | |
| 381 | + | echo "no tty available; run interactively" >&2 | |
| 382 | + | return 1 | |
| 383 | + | fi | |
| 384 | + | case "$input" in | |
| 385 | + | 1|i|I|install|Install) printf 'install\n'; return ;; | |
| 386 | + | 2|u|U|uninstall|Uninstall) printf 'uninstall\n'; return ;; | |
| 387 | + | *) echo " invalid input; enter 1 for install or 2 for uninstall." >&2 ;; | |
| 388 | + | esac | |
| 389 | + | done | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | # Locate product-info.json for a launcher and print dataDirectoryName. This | |
| 393 | + | # ties uninstalling to the IDE selected above instead of scanning every | |
| 394 | + | # JetBrains profile on the machine. | |
| 395 | + | product_data_name() { | |
| 396 | + | local launcher="$1" probe="$1" dir info parent raw | |
| 397 | + | ||
| 398 | + | # Resolve a symlinked Toolbox launcher when possible. | |
| 399 | + | if [[ -L "$probe" ]]; then | |
| 400 | + | raw="$(readlink "$probe" 2>/dev/null || true)" | |
| 401 | + | if [[ -n "$raw" ]]; then | |
| 402 | + | [[ "$raw" == /* ]] || raw="$(dirname "$probe")/$raw" | |
| 403 | + | probe="$raw" | |
| 404 | + | fi | |
| 405 | + | fi | |
| 406 | + | ||
| 407 | + | # Toolbox .cmd shims point at the real Windows launcher. Resolve that path | |
| 408 | + | # when running under WSL so product-info.json can be found beside the IDE. | |
| 409 | + | if [[ "$launcher" == *.cmd && -f "$launcher" ]]; then | |
| 410 | + | raw="$(tr -d '\r' <"$launcher" | sed -nE 's/.*"([A-Za-z]:\\[^"]+\.exe)".*/\1/p' | head -n 1)" | |
| 411 | + | if [[ -n "$raw" ]] && command -v wslpath >/dev/null 2>&1; then | |
| 412 | + | probe="$(wslpath -u "$raw" 2>/dev/null || printf '%s' "$launcher")" | |
| 413 | + | fi | |
| 414 | + | elif [[ "$OS_KIND" == "macos" && -f "$launcher" ]]; then | |
| 415 | + | # Toolbox shell launchers usually contain the real *.app executable path. | |
| 416 | + | raw="$(sed -nE 's/.*"([^\"]+\.app\/Contents\/MacOS\/[^\"]+)".*/\1/p' "$launcher" | head -n 1)" | |
| 417 | + | raw="${raw/#\~\//$HOME/}" | |
| 418 | + | raw="${raw/#\$HOME\//$HOME/}" | |
| 419 | + | [[ -n "$raw" && -e "$raw" ]] && probe="$raw" | |
| 420 | + | fi | |
| 421 | + | ||
| 422 | + | dir="$probe" | |
| 423 | + | [[ -f "$dir" ]] && dir="$(dirname "$dir")" | |
| 424 | + | while [[ -n "$dir" && "$dir" != "/" ]]; do | |
| 425 | + | for info in "$dir/product-info.json" "$dir/Resources/product-info.json"; do | |
| 426 | + | [[ -f "$info" ]] || continue | |
| 427 | + | sed -nE 's/.*"dataDirectoryName"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$info" | head -n 1 | |
| 428 | + | return | |
| 429 | + | done | |
| 430 | + | parent="$(dirname "$dir")" | |
| 431 | + | [[ "$parent" == "$dir" ]] && break | |
| 432 | + | dir="$parent" | |
| 433 | + | done | |
| 434 | + | } | |
| 435 | + | ||
| 436 | + | plugin_root_for_target() { | |
| 437 | + | local launcher="$1" type="$2" data_name | |
| 438 | + | data_name="$(product_data_name "$launcher")" | |
| 439 | + | [[ -n "$data_name" ]] || return 1 | |
| 440 | + | ||
| 441 | + | if [[ "$type" == "remotedev" ]]; then | |
| 442 | + | printf '%s/.local/shared/JetBrains/%s\n' "$HOME" "$data_name" | |
| 443 | + | elif [[ "$OS_KIND" == "macos" ]]; then | |
| 444 | + | printf '%s/Library/Application Support/JetBrains/%s/plugins\n' "$HOME" "$data_name" | |
| 445 | + | elif [[ "$OS_KIND" == "wsl2" && ( "$type" == "wincmd" || "$launcher" == /mnt/* ) ]]; then | |
| 446 | + | printf '%s/AppData/Roaming/JetBrains/%s/plugins\n' "$(win_user_dir)" "$data_name" | |
| 447 | + | else | |
| 448 | + | printf '%s/.local/share/JetBrains/%s\n' "$HOME" "$data_name" | |
| 449 | + | fi | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | xml_tag_value() { | |
| 453 | + | local xml="$1" tag="$2" rest | |
| 454 | + | [[ "$xml" == *"<$tag>"* ]] || return 1 | |
| 455 | + | rest="${xml#*<$tag>}" | |
| 456 | + | rest="${rest%%</$tag>*}" | |
| 457 | + | # plugin.xml values are normally plain text; trim surrounding whitespace. | |
| 458 | + | printf '%s' "$rest" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' | |
| 459 | + | } | |
| 460 | + | ||
| 461 | + | # Print "id|name" for one top-level item in an IDE's plugins directory. | |
| 462 | + | plugin_metadata() { | |
| 463 | + | local item="$1" xml="" archive id name | |
| 464 | + | if [[ -f "$item/META-INF/plugin.xml" ]]; then | |
| 465 | + | xml="$(tr '\n' ' ' <"$item/META-INF/plugin.xml")" | |
| 466 | + | elif [[ -f "$item" ]]; then | |
| 467 | + | xml="$(unzip -p "$item" META-INF/plugin.xml 2>/dev/null | tr '\n' ' ' || true)" | |
| 468 | + | elif [[ -d "$item" ]]; then | |
| 469 | + | while IFS= read -r -d '' archive; do | |
| 470 | + | xml="$(unzip -p "$archive" META-INF/plugin.xml 2>/dev/null | tr '\n' ' ' || true)" | |
| 471 | + | [[ -n "$xml" ]] && break | |
| 472 | + | done < <(find "$item" -maxdepth 3 -type f \( -name '*.jar' -o -name '*.zip' \) -print0 2>/dev/null) | |
| 473 | + | fi | |
| 474 | + | [[ -n "$xml" ]] || return 1 | |
| 475 | + | id="$(xml_tag_value "$xml" id || true)" | |
| 476 | + | name="$(xml_tag_value "$xml" name || true)" | |
| 477 | + | [[ -n "$id" ]] || id="$name" | |
| 478 | + | [[ -n "$name" ]] || name="$id" | |
| 479 | + | [[ -n "$id" ]] || return 1 | |
| 480 | + | printf '%s|%s\n' "$id" "$name" | |
| 481 | + | } | |
| 482 | + | ||
| 369 | 483 | # ------------------------------------------------------------------ | |
| 370 | 484 | # IDE picker | |
| 371 | 485 | # ------------------------------------------------------------------ | |
| @@ -395,20 +509,61 @@ if (( ${#SELECTED_IDES[@]} == 0 )); then | |||
| 395 | 509 | fi | |
| 396 | 510 | ||
| 397 | 511 | # ------------------------------------------------------------------ | |
| 398 | - | # Plugin picker | |
| 512 | + | # Action, then plugin picker | |
| 399 | 513 | # ------------------------------------------------------------------ | |
| 514 | + | ACTION="$(pick_action)" | |
| 515 | + | ||
| 400 | 516 | declare -a plugin_lines=() | |
| 401 | 517 | declare -A label_to_id | |
| 402 | - | for entry in "${PLUGIN_CATALOG[@]}"; do | |
| 403 | - | IFS='|' read -r pid plabel <<<"$entry" | |
| 404 | - | line="$(printf '%-32s %s' "$plabel" "$pid")" | |
| 405 | - | plugin_lines+=("$line") | |
| 406 | - | label_to_id[$line]="$pid" | |
| 407 | - | done | |
| 518 | + | declare -a INSTALLED_PLUGIN_RECORDS=() | |
| 519 | + | ||
| 520 | + | if [[ "$ACTION" == "install" ]]; then | |
| 521 | + | for entry in "${PLUGIN_CATALOG[@]}"; do | |
| 522 | + | IFS='|' read -r pid plabel <<<"$entry" | |
| 523 | + | line="$(printf '%-32s %s' "$plabel" "$pid")" | |
| 524 | + | plugin_lines+=("$line") | |
| 525 | + | label_to_id[$line]="$pid" | |
| 526 | + | done | |
| 527 | + | else | |
| 528 | + | if ! command -v unzip >/dev/null 2>&1; then | |
| 529 | + | echo "Uninstall requires 'unzip' so installed plugin IDs can be read safely." >&2 | |
| 530 | + | exit 1 | |
| 531 | + | fi | |
| 532 | + | ||
| 533 | + | declare -A seen_plugin_ids=() | |
| 534 | + | for ide_label in "${SELECTED_IDES[@]}"; do | |
| 535 | + | itype="${label_type[$ide_label]}" | |
| 536 | + | launcher="${label_launcher[$ide_label]}" | |
| 537 | + | if ! plugin_root="$(plugin_root_for_target "$launcher" "$itype")"; then | |
| 538 | + | echo "Could not resolve the plugin directory for: $ide_label" >&2 | |
| 539 | + | continue | |
| 540 | + | fi | |
| 541 | + | [[ -d "$plugin_root" ]] || continue | |
| 542 | + | ||
| 543 | + | while IFS= read -r -d '' item; do | |
| 544 | + | metadata="$(plugin_metadata "$item" || true)" | |
| 545 | + | [[ -n "$metadata" ]] || continue | |
| 546 | + | IFS='|' read -r pid plabel <<<"$metadata" | |
| 547 | + | INSTALLED_PLUGIN_RECORDS+=("${pid}|${plabel}|${item}|${plugin_root}|${ide_label}") | |
| 548 | + | if [[ -z "${seen_plugin_ids[$pid]+x}" ]]; then | |
| 549 | + | line="$(printf '%-32s %s' "$plabel" "$pid")" | |
| 550 | + | plugin_lines+=("$line") | |
| 551 | + | label_to_id[$line]="$pid" | |
| 552 | + | seen_plugin_ids[$pid]=1 | |
| 553 | + | fi | |
| 554 | + | done < <(find "$plugin_root" -mindepth 1 -maxdepth 1 -print0 2>/dev/null) | |
| 555 | + | done | |
| 556 | + | ||
| 557 | + | if (( ${#plugin_lines[@]} == 0 )); then | |
| 558 | + | echo "No removable user-installed plugins were found for the selected IDE(s)." >&2 | |
| 559 | + | echo "Bundled plugins cannot be uninstalled; disable them in the IDE instead." >&2 | |
| 560 | + | exit 0 | |
| 561 | + | fi | |
| 562 | + | fi | |
| 408 | 563 | ||
| 409 | 564 | mapfile -t SELECTED_PLUGIN_LINES < <( | |
| 410 | 565 | printf '%s\n' "${plugin_lines[@]}" | | |
| 411 | - | pick_multi "Plugins>" "Pick plugin(s) — TAB to toggle (fzf) or spaced indexes" | |
| 566 | + | pick_multi "Plugins>" "Pick plugin(s) to ${ACTION} — TAB to toggle (fzf) or spaced indexes" | |
| 412 | 567 | ) | |
| 413 | 568 | ||
| 414 | 569 | if (( ${#SELECTED_PLUGIN_LINES[@]} == 0 )); then | |
| @@ -468,7 +623,7 @@ while :; do | |||
| 468 | 623 | ||
| 469 | 624 | { | |
| 470 | 625 | echo | |
| 471 | - | echo "The following IDE(s) appear to be running — installPlugins needs them closed:" | |
| 626 | + | echo "The following IDE(s) appear to be running — plugin changes need them closed:" | |
| 472 | 627 | for r in "${_running[@]}"; do | |
| 473 | 628 | echo " - $r" | |
| 474 | 629 | done | |
| @@ -482,8 +637,64 @@ while :; do | |||
| 482 | 637 | done | |
| 483 | 638 | ||
| 484 | 639 | # ------------------------------------------------------------------ | |
| 485 | - | # Install loop | |
| 640 | + | # Install/uninstall loop | |
| 486 | 641 | # ------------------------------------------------------------------ | |
| 642 | + | if [[ "$ACTION" == "uninstall" ]]; then | |
| 643 | + | declare -A selected_id_map=() | |
| 644 | + | for pid in "${SELECTED_PLUGIN_IDS[@]}"; do selected_id_map[$pid]=1; done | |
| 645 | + | ||
| 646 | + | declare -a TO_REMOVE=() | |
| 647 | + | for record in "${INSTALLED_PLUGIN_RECORDS[@]}"; do | |
| 648 | + | IFS='|' read -r pid plabel plugin_path plugin_root ide_label <<<"$record" | |
| 649 | + | [[ -n "${selected_id_map[$pid]+x}" ]] || continue | |
| 650 | + | # Safety boundary: only top-level children found in the resolved plugin root. | |
| 651 | + | [[ "$(dirname "$plugin_path")" == "$plugin_root" ]] || continue | |
| 652 | + | TO_REMOVE+=("$record") | |
| 653 | + | done | |
| 654 | + | ||
| 655 | + | if (( ${#TO_REMOVE[@]} == 0 )); then | |
| 656 | + | echo "None of the selected plugins are installed in the selected IDE(s)." >&2 | |
| 657 | + | exit 0 | |
| 658 | + | fi | |
| 659 | + | ||
| 660 | + | echo | |
| 661 | + | echo "The following plugin installations will be removed:" | |
| 662 | + | for record in "${TO_REMOVE[@]}"; do | |
| 663 | + | IFS='|' read -r pid plabel plugin_path plugin_root ide_label <<<"$record" | |
| 664 | + | echo " - $plabel ($pid)" | |
| 665 | + | echo " IDE: $ide_label" | |
| 666 | + | echo " Path: $plugin_path" | |
| 667 | + | done | |
| 668 | + | printf 'Continue? [y/N]: ' >&2 | |
| 669 | + | if ! IFS= read -r confirm </dev/tty || [[ ! "$confirm" =~ ^[Yy]$ ]]; then | |
| 670 | + | echo "Uninstall cancelled." | |
| 671 | + | exit 0 | |
| 672 | + | fi | |
| 673 | + | ||
| 674 | + | removed=0 | |
| 675 | + | failed=0 | |
| 676 | + | declare -a FAILED_LIST=() | |
| 677 | + | for record in "${TO_REMOVE[@]}"; do | |
| 678 | + | IFS='|' read -r pid plabel plugin_path plugin_root ide_label <<<"$record" | |
| 679 | + | printf 'Removing %-32s from %s... ' "$plabel" "$ide_label" | |
| 680 | + | if rm -rf -- "$plugin_path"; then | |
| 681 | + | echo "ok" | |
| 682 | + | removed=$((removed + 1)) | |
| 683 | + | else | |
| 684 | + | echo "FAILED" | |
| 685 | + | failed=$((failed + 1)) | |
| 686 | + | FAILED_LIST+=("${ide_label} :: ${pid}") | |
| 687 | + | fi | |
| 688 | + | done | |
| 689 | + | ||
| 690 | + | echo "Done. Removed: ${removed}, Failed: ${failed}. Restart the IDE(s) to finish." | |
| 691 | + | if (( failed > 0 )); then | |
| 692 | + | printf ' - %s\n' "${FAILED_LIST[@]}" >&2 | |
| 693 | + | exit 1 | |
| 694 | + | fi | |
| 695 | + | exit 0 | |
| 696 | + | fi | |
| 697 | + | ||
| 487 | 698 | LOG_FILE="$(mktemp -t jb-install.XXXXXX)" | |
| 488 | 699 | trap 'rm -f "$LOG_FILE"' EXIT | |
| 489 | 700 | ||
| @@ -556,4 +767,4 @@ if (( failed > 0 )); then | |||
| 556 | 767 | echo " - $f" >&2 | |
| 557 | 768 | done | |
| 558 | 769 | exit 1 | |
| 559 | - | fi | |
| 770 | + | fi | |
weehong zrewidował ten Gist 1 month ago. Przejdź do rewizji
1 file changed, 1 insertion, 1 deletion
ideavimrc
| @@ -34,7 +34,7 @@ Plug 'terryma/vim-multiple-cursors' | |||
| 34 | 34 | ||
| 35 | 35 | " --- Better IdeaVim / IDE integration --- | |
| 36 | 36 | set ideajoin | |
| 37 | - | set ideaput | |
| 37 | + | set clipboard+=unnamedplus | |
| 38 | 38 | set ideamarks | |
| 39 | 39 | ||
| 40 | 40 | " --- Optional built-in IdeaVim extensions --- | |
Vernon Wee Hong KOH zrewidował ten Gist 1 month ago. Przejdź do rewizji
1 file changed, 270 insertions
ideavim-cheatsheet.html(stworzono plik)
| @@ -0,0 +1,270 @@ | |||
| 1 | + | <!doctype html> | |
| 2 | + | <html lang="en"> | |
| 3 | + | <head> | |
| 4 | + | <meta charset="utf-8"> | |
| 5 | + | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| 6 | + | <title>IdeaVim Cheatsheet</title> | |
| 7 | + | <style> | |
| 8 | + | @page { | |
| 9 | + | size: A4; | |
| 10 | + | margin: 14mm; | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | * { | |
| 14 | + | box-sizing: border-box; | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | body { | |
| 18 | + | margin: 0; | |
| 19 | + | color: #15171a; | |
| 20 | + | background: #ffffff; | |
| 21 | + | font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| 22 | + | font-size: 10.5px; | |
| 23 | + | line-height: 1.35; | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | header { | |
| 27 | + | margin-bottom: 14px; | |
| 28 | + | padding-bottom: 10px; | |
| 29 | + | border-bottom: 2px solid #15171a; | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | h1 { | |
| 33 | + | margin: 0 0 3px; | |
| 34 | + | font-size: 25px; | |
| 35 | + | line-height: 1.1; | |
| 36 | + | letter-spacing: 0; | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | .subtitle { | |
| 40 | + | margin: 0; | |
| 41 | + | color: #4d5662; | |
| 42 | + | font-size: 11px; | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | .grid { | |
| 46 | + | display: grid; | |
| 47 | + | grid-template-columns: 1fr 1fr; | |
| 48 | + | gap: 11px 14px; | |
| 49 | + | align-items: start; | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | section { | |
| 53 | + | break-inside: avoid; | |
| 54 | + | page-break-inside: avoid; | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | h2 { | |
| 58 | + | margin: 0 0 5px; | |
| 59 | + | padding: 5px 7px; | |
| 60 | + | color: #ffffff; | |
| 61 | + | background: #243447; | |
| 62 | + | border-radius: 4px; | |
| 63 | + | font-size: 11px; | |
| 64 | + | line-height: 1.2; | |
| 65 | + | letter-spacing: 0; | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | table { | |
| 69 | + | width: 100%; | |
| 70 | + | border-collapse: collapse; | |
| 71 | + | table-layout: fixed; | |
| 72 | + | border: 1px solid #d5dbe3; | |
| 73 | + | border-radius: 4px; | |
| 74 | + | overflow: hidden; | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | th, | |
| 78 | + | td { | |
| 79 | + | padding: 5px 6px; | |
| 80 | + | vertical-align: top; | |
| 81 | + | border-bottom: 1px solid #e5e9ef; | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | tr:last-child td { | |
| 85 | + | border-bottom: 0; | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | th { | |
| 89 | + | color: #4d5662; | |
| 90 | + | background: #f4f6f8; | |
| 91 | + | font-weight: 650; | |
| 92 | + | text-align: left; | |
| 93 | + | font-size: 9px; | |
| 94 | + | text-transform: uppercase; | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | th:first-child, | |
| 98 | + | td:first-child { | |
| 99 | + | width: 33%; | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | code { | |
| 103 | + | display: inline-block; | |
| 104 | + | padding: 1px 4px; | |
| 105 | + | color: #0f1720; | |
| 106 | + | background: #eef2f7; | |
| 107 | + | border: 1px solid #dce3ec; | |
| 108 | + | border-radius: 3px; | |
| 109 | + | font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace; | |
| 110 | + | font-size: 9.5px; | |
| 111 | + | line-height: 1.25; | |
| 112 | + | white-space: nowrap; | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | .note { | |
| 116 | + | margin: 10px 0 0; | |
| 117 | + | padding: 7px 8px; | |
| 118 | + | color: #313942; | |
| 119 | + | background: #fff7e1; | |
| 120 | + | border: 1px solid #ead48a; | |
| 121 | + | border-radius: 4px; | |
| 122 | + | font-size: 10px; | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | .wide { | |
| 126 | + | grid-column: 1 / -1; | |
| 127 | + | } | |
| 128 | + | </style> | |
| 129 | + | </head> | |
| 130 | + | <body> | |
| 131 | + | <header> | |
| 132 | + | <h1>IdeaVim Cheatsheet</h1> | |
| 133 | + | <p class="subtitle">Leader key: <code>Space</code>. Example: <code><leader>ff</code> means <code>Space f f</code>.</p> | |
| 134 | + | </header> | |
| 135 | + | ||
| 136 | + | <main class="grid"> | |
| 137 | + | <section> | |
| 138 | + | <h2>Navigation</h2> | |
| 139 | + | <table> | |
| 140 | + | <thead><tr><th>Key</th><th>Action</th></tr></thead> | |
| 141 | + | <tbody> | |
| 142 | + | <tr><td><code>gd</code></td><td>Go to declaration</td></tr> | |
| 143 | + | <tr><td><code>gi</code></td><td>Go to implementation</td></tr> | |
| 144 | + | <tr><td><code>gr</code></td><td>Find usages</td></tr> | |
| 145 | + | <tr><td><code>gb</code></td><td>Go back</td></tr> | |
| 146 | + | <tr><td><code>gB</code></td><td>Go forward</td></tr> | |
| 147 | + | </tbody> | |
| 148 | + | </table> | |
| 149 | + | </section> | |
| 150 | + | ||
| 151 | + | <section> | |
| 152 | + | <h2>Files And Search</h2> | |
| 153 | + | <table> | |
| 154 | + | <thead><tr><th>Key</th><th>Action</th></tr></thead> | |
| 155 | + | <tbody> | |
| 156 | + | <tr><td><code>Space ff</code></td><td>Go to file</td></tr> | |
| 157 | + | <tr><td><code>Space fs</code></td><td>Find in project</td></tr> | |
| 158 | + | <tr><td><code>Space fa</code></td><td>Go to IDE action</td></tr> | |
| 159 | + | <tr><td><code>Space fr</code></td><td>Recent files</td></tr> | |
| 160 | + | <tr><td><code>Space e</code></td><td>Select current file in project view</td></tr> | |
| 161 | + | </tbody> | |
| 162 | + | </table> | |
| 163 | + | </section> | |
| 164 | + | ||
| 165 | + | <section> | |
| 166 | + | <h2>Code Actions</h2> | |
| 167 | + | <table> | |
| 168 | + | <thead><tr><th>Key</th><th>Action</th></tr></thead> | |
| 169 | + | <tbody> | |
| 170 | + | <tr><td><code>Space ca</code></td><td>Show intention actions / quick fix</td></tr> | |
| 171 | + | <tr><td><code>Space cf</code></td><td>Reformat code</td></tr> | |
| 172 | + | <tr><td><code>Space co</code></td><td>Optimize imports</td></tr> | |
| 173 | + | <tr><td><code>Space R</code></td><td>Rename symbol</td></tr> | |
| 174 | + | </tbody> | |
| 175 | + | </table> | |
| 176 | + | </section> | |
| 177 | + | ||
| 178 | + | <section> | |
| 179 | + | <h2>Run And Debug</h2> | |
| 180 | + | <table> | |
| 181 | + | <thead><tr><th>Key</th><th>Action</th></tr></thead> | |
| 182 | + | <tbody> | |
| 183 | + | <tr><td><code>Space rr</code></td><td>Run</td></tr> | |
| 184 | + | <tr><td><code>Space dd</code></td><td>Debug</td></tr> | |
| 185 | + | <tr><td><code>Space db</code></td><td>Toggle breakpoint</td></tr> | |
| 186 | + | </tbody> | |
| 187 | + | </table> | |
| 188 | + | </section> | |
| 189 | + | ||
| 190 | + | <section> | |
| 191 | + | <h2>Tool Windows</h2> | |
| 192 | + | <table> | |
| 193 | + | <thead><tr><th>Key</th><th>Action</th></tr></thead> | |
| 194 | + | <tbody> | |
| 195 | + | <tr><td><code>Space tt</code></td><td>Open terminal</td></tr> | |
| 196 | + | <tr><td><code>Space hh</code></td><td>Hide all tool windows</td></tr> | |
| 197 | + | </tbody> | |
| 198 | + | </table> | |
| 199 | + | </section> | |
| 200 | + | ||
| 201 | + | <section> | |
| 202 | + | <h2>Completion</h2> | |
| 203 | + | <table> | |
| 204 | + | <thead><tr><th>Key</th><th>Action</th></tr></thead> | |
| 205 | + | <tbody> | |
| 206 | + | <tr><td><code>Tab</code></td><td>Next completion item</td></tr> | |
| 207 | + | <tr><td><code>Shift+Tab</code></td><td>Previous completion item</td></tr> | |
| 208 | + | </tbody> | |
| 209 | + | </table> | |
| 210 | + | </section> | |
| 211 | + | ||
| 212 | + | <section class="wide"> | |
| 213 | + | <h2>Surround</h2> | |
| 214 | + | <table> | |
| 215 | + | <thead><tr><th>Key</th><th>Example / Meaning</th></tr></thead> | |
| 216 | + | <tbody> | |
| 217 | + | <tr><td><code>cs"'</code></td><td>Change <code>"hello"</code> to <code>'hello'</code></td></tr> | |
| 218 | + | <tr><td><code>cs({</code></td><td>Change <code>(x)</code> to <code>{ x }</code></td></tr> | |
| 219 | + | <tr><td><code>ds"</code></td><td>Delete surrounding quotes from <code>"hello"</code></td></tr> | |
| 220 | + | <tr><td><code>ysiw"</code></td><td>Surround current word with quotes</td></tr> | |
| 221 | + | <tr><td><code>yss)</code></td><td>Surround the whole line with parentheses</td></tr> | |
| 222 | + | </tbody> | |
| 223 | + | </table> | |
| 224 | + | </section> | |
| 225 | + | ||
| 226 | + | <section> | |
| 227 | + | <h2>Text Objects</h2> | |
| 228 | + | <table> | |
| 229 | + | <thead><tr><th>Key</th><th>Action</th></tr></thead> | |
| 230 | + | <tbody> | |
| 231 | + | <tr><td><code>ciq</code></td><td>Change inside any quote</td></tr> | |
| 232 | + | <tr><td><code>caq</code></td><td>Change around any quote</td></tr> | |
| 233 | + | <tr><td><code>cib</code></td><td>Change inside any bracket</td></tr> | |
| 234 | + | <tr><td><code>cab</code></td><td>Change around any bracket</td></tr> | |
| 235 | + | <tr><td><code>diq</code></td><td>Delete inside any quote</td></tr> | |
| 236 | + | <tr><td><code>dib</code></td><td>Delete inside any bracket</td></tr> | |
| 237 | + | <tr><td><code>viq</code></td><td>Select inside any quote</td></tr> | |
| 238 | + | <tr><td><code>vib</code></td><td>Select inside any bracket</td></tr> | |
| 239 | + | </tbody> | |
| 240 | + | </table> | |
| 241 | + | </section> | |
| 242 | + | ||
| 243 | + | <section> | |
| 244 | + | <h2>Multi-Cursor</h2> | |
| 245 | + | <table> | |
| 246 | + | <thead><tr><th>Key</th><th>Action</th></tr></thead> | |
| 247 | + | <tbody> | |
| 248 | + | <tr><td><code>Ctrl+n</code></td><td>Select next occurrence of word</td></tr> | |
| 249 | + | <tr><td><code>g Ctrl+n</code></td><td>Select next occurrence, not whole word</td></tr> | |
| 250 | + | <tr><td><code>Alt+n</code></td><td>Select all occurrences in file</td></tr> | |
| 251 | + | <tr><td><code>Ctrl+x</code></td><td>Skip current occurrence</td></tr> | |
| 252 | + | <tr><td><code>Ctrl+p</code></td><td>Remove current occurrence</td></tr> | |
| 253 | + | </tbody> | |
| 254 | + | </table> | |
| 255 | + | </section> | |
| 256 | + | ||
| 257 | + | <section class="wide"> | |
| 258 | + | <h2>Reload Config</h2> | |
| 259 | + | <table> | |
| 260 | + | <thead><tr><th>Key / Command</th><th>Action</th></tr></thead> | |
| 261 | + | <tbody> | |
| 262 | + | <tr><td><code>Ctrl+Shift+O</code></td><td>Reload <code>~/.ideavimrc</code> from JetBrains</td></tr> | |
| 263 | + | <tr><td><code>:source ~/.ideavimrc</code></td><td>Reload <code>~/.ideavimrc</code> from Vim command mode</td></tr> | |
| 264 | + | </tbody> | |
| 265 | + | </table> | |
| 266 | + | <p class="note">EasyMotion requires the JetBrains IdeaVim-EasyMotion and AceJump IDE plugins.</p> | |
| 267 | + | </section> | |
| 268 | + | </main> | |
| 269 | + | </body> | |
| 270 | + | </html> | |
Vernon Wee Hong KOH zrewidował ten Gist 1 month ago. Przejdź do rewizji
1 file changed, 0 insertions, 0 deletions
.ideavimrc zmieniono nazwę na ideavimrc
Zmieniono nazwę pliku bez modyfikacji zawartości
Vernon Wee Hong KOH zrewidował ten Gist 1 month ago. Przejdź do rewizji
1 file changed, 82 insertions
.ideavimrc(stworzono plik)
| @@ -0,0 +1,82 @@ | |||
| 1 | + | " .ideavimrc is a configuration file for IdeaVim plugin. It uses | |
| 2 | + | " the same commands as the original .vimrc configuration. | |
| 3 | + | " You can find a list of commands here: https://jb.gg/h38q75 | |
| 4 | + | " Find more examples here: https://jb.gg/share-ideavimrc | |
| 5 | + | ||
| 6 | + | " Source your .vimrc | |
| 7 | + | source ~/.vimrc | |
| 8 | + | ||
| 9 | + | "" -- Suggested options -- | |
| 10 | + | " Show a few lines of context around the cursor. Note that this makes the | |
| 11 | + | " text scroll if you mouse-click near the start or end of the window. | |
| 12 | + | set scrolloff=5 | |
| 13 | + | ||
| 14 | + | " Do incremental searching. | |
| 15 | + | set incsearch | |
| 16 | + | ||
| 17 | + | " Don't use Ex mode, use Q for formatting. | |
| 18 | + | map Q gq | |
| 19 | + | ||
| 20 | + | " --- Enable IdeaVim plugins https://jb.gg/ideavim-plugins | |
| 21 | + | ||
| 22 | + | " Highlight copied text | |
| 23 | + | Plug 'machakann/vim-highlightedyank' | |
| 24 | + | " Commentary plugin | |
| 25 | + | Plug 'tpope/vim-commentary' | |
| 26 | + | " Surround text objects and changes | |
| 27 | + | Plug 'tpope/vim-surround' | |
| 28 | + | " Better text objects around quotes, brackets, arguments, and separators | |
| 29 | + | Plug 'wellle/targets.vim' | |
| 30 | + | " Fast cursor jumps. Requires the IdeaVim-EasyMotion and AceJump IDE plugins. | |
| 31 | + | Plug 'easymotion/vim-easymotion' | |
| 32 | + | " Multi-cursor editing | |
| 33 | + | Plug 'terryma/vim-multiple-cursors' | |
| 34 | + | ||
| 35 | + | " --- Better IdeaVim / IDE integration --- | |
| 36 | + | set ideajoin | |
| 37 | + | set ideaput | |
| 38 | + | set ideamarks | |
| 39 | + | ||
| 40 | + | " --- Optional built-in IdeaVim extensions --- | |
| 41 | + | set mini-ai | |
| 42 | + | set youcompleteme | |
| 43 | + | ||
| 44 | + | " --- IDE navigation --- | |
| 45 | + | nmap gd <Action>(GotoDeclaration) | |
| 46 | + | nmap gi <Action>(GotoImplementation) | |
| 47 | + | nmap gr <Action>(FindUsages) | |
| 48 | + | nmap gb <Action>(Back) | |
| 49 | + | nmap gB <Action>(Forward) | |
| 50 | + | ||
| 51 | + | " --- Search / files / project --- | |
| 52 | + | nmap <leader>ff <Action>(GotoFile) | |
| 53 | + | nmap <leader>fs <Action>(FindInPath) | |
| 54 | + | nmap <leader>fa <Action>(GotoAction) | |
| 55 | + | nmap <leader>fr <Action>(RecentFiles) | |
| 56 | + | nmap <leader>e <Action>(SelectInProjectView) | |
| 57 | + | ||
| 58 | + | " --- Code actions --- | |
| 59 | + | nmap <leader>ca <Action>(ShowIntentionActions) | |
| 60 | + | nmap <leader>cf <Action>(ReformatCode) | |
| 61 | + | nmap <leader>co <Action>(OptimizeImports) | |
| 62 | + | nmap <leader>R <Action>(RenameElement) | |
| 63 | + | ||
| 64 | + | " --- Run / debug --- | |
| 65 | + | nmap <leader>rr <Action>(Run) | |
| 66 | + | nmap <leader>dd <Action>(Debug) | |
| 67 | + | nmap <leader>db <Action>(ToggleLineBreakpoint) | |
| 68 | + | ||
| 69 | + | " --- Tool windows --- | |
| 70 | + | nmap <leader>tt <Action>(ActivateTerminalToolWindow) | |
| 71 | + | nmap <leader>hh <Action>(HideAllWindows) | |
| 72 | + | ||
| 73 | + | ||
| 74 | + | "" -- Map IDE actions to IdeaVim -- https://jb.gg/abva4t | |
| 75 | + | "" Map \r to the Reformat Code action | |
| 76 | + | "map \r <Action>(ReformatCode) | |
| 77 | + | ||
| 78 | + | "" Map <leader>d to start debug | |
| 79 | + | "map <leader>d <Action>(Debug) | |
| 80 | + | ||
| 81 | + | "" Map \b to toggle the breakpoint on the current line | |
| 82 | + | "map \b <Action>(ToggleLineBreakpoint) | |
Vernon Wee Hong KOH zrewidował ten Gist 1 month ago. Przejdź do rewizji
1 file changed, 17 insertions, 9 deletions
install.sh
| @@ -120,7 +120,10 @@ fi | |||
| 120 | 120 | ||
| 121 | 121 | # 4. Extract extensions from JSON using jq (if not using hardcoded array) | |
| 122 | 122 | if [[ "$USE_HARDCODED" == false ]]; then | |
| 123 | - | mapfile -t EXTENSIONS < <(jq -r '.recommendations[]?' "$JSON_FILE") | |
| 123 | + | EXTENSIONS=() | |
| 124 | + | while IFS= read -r ext; do | |
| 125 | + | EXTENSIONS+=("$ext") | |
| 126 | + | done < <(jq -r '.recommendations[]?' "$JSON_FILE") | |
| 124 | 127 | ||
| 125 | 128 | if [[ ${#EXTENSIONS[@]} -eq 0 ]]; then | |
| 126 | 129 | echo "Error: No extensions found under the 'recommendations' key." >&2 | |
| @@ -128,14 +131,19 @@ if [[ "$USE_HARDCODED" == false ]]; then | |||
| 128 | 131 | fi | |
| 129 | 132 | fi | |
| 130 | 133 | ||
| 131 | - | declare -a CODE_ARGS=() | |
| 132 | - | if [[ -n "$PROFILE" ]]; then | |
| 133 | - | CODE_ARGS+=("--profile" "$PROFILE") | |
| 134 | - | fi | |
| 135 | - | ||
| 136 | 134 | # Trap to clean up logs and temporary downloaded JSON files | |
| 137 | 135 | LOG_FILE="$(mktemp -t vscode-install.XXXXXX)" | |
| 138 | - | trap 'rm -f "$LOG_FILE" $TEMP_JSON_FILE' EXIT | |
| 136 | + | trap 'rm -f "$LOG_FILE" "${TEMP_JSON_FILE:-}"' EXIT | |
| 137 | + | ||
| 138 | + | install_extension() { | |
| 139 | + | local ext="$1" | |
| 140 | + | ||
| 141 | + | if [[ -n "$PROFILE" ]]; then | |
| 142 | + | code --profile "$PROFILE" --install-extension "$ext" --force | |
| 143 | + | else | |
| 144 | + | code --install-extension "$ext" --force | |
| 145 | + | fi | |
| 146 | + | } | |
| 139 | 147 | ||
| 140 | 148 | header="Installing ${#EXTENSIONS[@]} extension(s)" | |
| 141 | 149 | [[ -n "$PROFILE" ]] && header+=" into profile '$PROFILE'" | |
| @@ -150,7 +158,7 @@ for ext in "${EXTENSIONS[@]}"; do | |||
| 150 | 158 | [[ -z "$ext" ]] && continue | |
| 151 | 159 | ||
| 152 | 160 | printf ' -> %-55s ' "$ext" | |
| 153 | - | if code "${CODE_ARGS[@]}" --install-extension "$ext" --force >"$LOG_FILE" 2>&1; then | |
| 161 | + | if install_extension "$ext" >"$LOG_FILE" 2>&1; then | |
| 154 | 162 | echo "ok" | |
| 155 | 163 | installed=$((installed + 1)) | |
| 156 | 164 | else | |
| @@ -170,4 +178,4 @@ if (( failed > 0 )); then | |||
| 170 | 178 | echo " - $f" >&2 | |
| 171 | 179 | done | |
| 172 | 180 | exit 1 | |
| 173 | - | fi | |
| 181 | + | fi | |