Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,62 @@
# Changelog

## [0.1.5] — 2026-07-06

### Added

- **Doctor diagnostics action** — new `DoctorAction` that collects and displays plugin setup diagnostics:
project root, Gemfile status, Ruby SDK path, docscribe gem version, daemon server state, and
settings. Accessible from Editor Popup DocScribe DocScribe Doctor. Reports actionable steps
for common issues (missing gem, no SDK, no Gemfile).
- **Capability detection for docscribe version** — `performGemCheck()` now parses `--version` output
and stores parsed version + capabilities (`serverMode` for ≥ 1.5.1). `ensureRunning()` skips
server startup when version < 1.5.1, falling back directly to CLI. New `parseVersion()` utility
in companion object with 8 unit tests.
- **Rakefile support (no extension)** — all annotators, actions, and intention actions now accept
a file named `Rakefile` (without `.rb` or `.rake` extension) in addition to `.rb` and `.rake`
files. This ensures the plugin works on standard Rakefiles that have no file extension.
- **Graceful handling when docscribe gem not installed** — `DocscribeDaemon` now runs
`bundle exec docscribe --version` once on first use and caches the result. If the gem is missing, a
user-friendly notification (with "Open Gemfile" action) is shown once, and all operations return a
clear error: *"Add 'gem \"docscribe\"' to your Gemfile and run 'bundle install'"*.
- **Tests for gem availability check** — 5 new tests in `DocscribeDaemonGemTest.kt` covering MISSING
status, error message format, all command types, and status caching.

### Changed

- **Rapid annotations now discard stale results** — when the IDE triggers multiple checks for the
same file in quick succession (e.g. series of saves), each file has a generation counter. If a
newer check starts while an older one is still running, the older result is discarded on
completion. Only the last check's annotations are applied to the editor (see
`DocscribeAnnotator.fileGeneration`).
- **All commands now use project Ruby SDK** — `bundle exec docscribe` (via `DocscribeRunner`
and `DocscribeDaemon.fallback()`) and `bundle exec docscribe --version` (via
`DocscribeDaemon.performGemCheck()`) now prepend the Ruby SDK's `bin/` directory to `PATH`
and set `BUNDLE_GEMFILE`. This ensures the SDK's Ruby and Bundler are used instead of system
defaults. Extracted shared `buildSdkEnvironment()` helper, also reused in server startup.
- **`com.intellij.modules.ruby` made optional** — changed from hard `<depends>` to
`<depends optional="true" config-file="withRubyPlugin.xml">`. Ruby-specific extensions
(`ExternalAnnotator`, `FoldingBuilder`, `DependencySupport`, `IntentionAction`) moved to a new
secondary descriptor. Plugin now installs on IntelliJ IDEA without Ruby plugin (limited
functionality), unblocking Marketplace publication.
- **Annotation cache now respects settings** — `configHash` in `AnnotatorFileInfo` is now derived
from `DocscribeSettings.hideCommentsByDefault` instead of being hardcoded to `0`. Changing
settings automatically invalidates cached annotations. Cache also has a max size (1000 entries)
with LRU-like eviction.
- **Version** bumped from `0.1.4` to `0.1.5`.

### Fixed

- **Annotation cache never invalidated on settings change** — `DocscribeSettingsChangeListener`
now calls `DocscribeAnnotatorCache.clear()` when settings are saved, in addition to refreshing
code folding.

### Build

- **`intellijDependencies()`** — added to repository section in `build.gradle.kts` (required by
`instrumentTestCode` task).
- **Total test count** — 119 tests across 18 test files, all passing.

## [0.1.4] — 2026-06-29

### Fixed
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

**DocScribe** is a RubyMine plugin that auto-generates inline YARD documentation for Ruby methods
using [docscribe](https://github.com/unurgunite/docscribe) — a Ruby gem that analyzes AST and suggests YARD-compatible
documentation. Compatible with **docscribe >= 1.4.0** (daemon mode requires >= 1.5.0).
documentation. Compatible with **docscribe >= 1.4.0** (daemon mode requires >= 1.5.1).

> Also, available for [VS Code](https://github.com/FlorexLabs/docscribe-vscode) on
> the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=FlorexLabs.docscribe).
Expand Down Expand Up @@ -60,7 +60,8 @@ documentation. Compatible with **docscribe >= 1.4.0** (daemon mode requires >= 1
- **Update types from RBS** — refresh YARD docs from RBS signatures
- **Collapsible YARD docs** — fold all YARD comment blocks automatically on file open (configurable in settings)
- **Configurable** — hide comments by default
- **`.rake` support** — diagnostics and actions work on Rake files
- **`.rake` and `Rakefile` support** — diagnostics and actions work on Rake files and bare Rakefiles
- **Automatic backend selection** — detects docscribe version on first use, chooses daemon (>= 1.5.1) or CLI fallback
- **JSON output** — uses `docscribe --format json` for reliable diagnostics parsing

## Requirements
Expand All @@ -73,11 +74,11 @@ documentation. Compatible with **docscribe >= 1.4.0** (daemon mode requires >= 1

| Mode | docscribe version | Ruby version |
|--------------------------|-------------------|--------------|
| Daemon (Unix socket RPC) | >= 1.5.0 | >= 3.0 |
| Daemon (Unix socket RPC) | >= 1.5.1 | >= 3.0 |
| CLI fallback | >= 1.4.0 | >= 2.7 |

The plugin prefers daemon mode for better performance. If `docscribe < 1.5.0` or the Ruby SDK is unavailable, it
automatically falls back to spawning the CLI directly.
The plugin automatically selects the backend based on the detected docscribe version: daemon mode for >= 1.5.1,
CLI fallback for older versions. If the Ruby SDK is unavailable, it falls back to system PATH Ruby.

```bash
gem install docscribe
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ repositories {
mavenCentral()
intellijPlatform {
defaultRepositories()
intellijDependencies()
}
}

Expand Down
1 change: 0 additions & 1 deletion config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,3 @@ style:
complexity:
CyclomaticComplexMethod:
active: true
allowedComplexity: 20
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pluginGroup = com.florexlabs
pluginName = docscribe-rubymine
pluginVersion = 0.1.4
pluginVersion = 0.1.5
pluginSinceBuild = 261
pluginUntilBuild = 261.*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ class AggressiveFixAction : AnAction() {
*/
override fun update(e: AnActionEvent) {
val file = e.getData(CommonDataKeys.VIRTUAL_FILE)
e.presentation.isEnabledAndVisible = file != null && (file.name.endsWith(".rb") || file.name.endsWith(".rake"))
e.presentation.isEnabledAndVisible =
file != null && (file.name.endsWith(".rb") || file.name.endsWith(".rake") || file.name == "Rakefile")
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ class CheckFileAction : AnAction() {
*/
override fun update(e: AnActionEvent) {
val file = e.getData(CommonDataKeys.VIRTUAL_FILE)
e.presentation.isEnabledAndVisible = file != null && (file.name.endsWith(".rb") || file.name.endsWith(".rake"))
e.presentation.isEnabledAndVisible =
file != null && (file.name.endsWith(".rb") || file.name.endsWith(".rake") || file.name == "Rakefile")
}

/**
Expand Down
191 changes: 191 additions & 0 deletions src/main/kotlin/com/florexlabs/docscribe/actions/DoctorAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package com.florexlabs.docscribe.actions

import com.florexlabs.docscribe.runner.DocscribeDaemon
import com.florexlabs.docscribe.runner.DocscribeDaemon.DocscribeCapabilities
import com.florexlabs.docscribe.runner.DocscribeDaemon.DocscribeStatus
import com.florexlabs.docscribe.runner.DocscribeRunner
import com.florexlabs.docscribe.settings.DocscribeSettings
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import java.io.File

/**
* Diagnose DocScribe plugin setup and show a detailed report.
*
* Collects: project root, Gemfile, Ruby SDK, docscribe gem status/version,
* daemon server state, plugin settings. Calls none of the query methods
* trigger side effects — they read cached state.
*/
class DoctorAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val report = buildReport(project)
notify(project, report)
}

/**
* Enable action for any open project.
*/
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = e.project != null
}

/**
* Always use a background thread for update checks.
*/
override fun getActionUpdateThread() = ActionUpdateThread.BGT

/**
* Build a multi-line diagnostic report string.
*
* @param project The current project.
* @return Formatted diagnostic text with sections.
*/
private fun buildReport(project: Project): String {
val daemon = DocscribeDaemon.getInstance(project)
val basePath = project.basePath ?: "?"
val gemRoot = DocscribeRunner.findProjectRoot(basePath)
val rubyPath = daemon.getRubyPath()
val status = daemon.getDocscribeStatus()
val caps = daemon.getCapabilities()

val lines = mutableListOf("=== DocScribe Diagnostics ===\n")
lines += projectInfoLines(basePath, gemRoot)
lines += ""
lines += rubySdkLines(project, daemon)
lines += ""
lines += gemInfoLines(status, caps)
lines += ""
lines += daemonInfoLines(project, status, caps)
lines += ""
val settings = DocscribeSettings.getInstance()
lines += listOf("Settings:", " hideCommentsByDefault = ${settings.hideCommentsByDefault}")
lines += ""
lines += issuesSummaryLines(rubyPath, gemRoot, status)
return lines.joinToString("\n")
}

/**
* Project root and Gemfile status.
*/
private fun projectInfoLines(
basePath: String,
gemRoot: String?,
): List<String> {
val lines = mutableListOf("Project root: $basePath")
val gemFile = if (gemRoot != null) File(gemRoot, "Gemfile") else null
val gemStatus =
when {
gemFile != null && gemFile.exists() -> "found ($gemFile)"
gemFile != null -> "Gemfile path exists but file missing: $gemFile"
else -> "not found (no Gemfile in tree)"
}
lines += "Gemfile: $gemStatus"
return lines
}

/**
* IDE Ruby SDK and resolved Ruby binary path.
*/
private fun rubySdkLines(
project: Project,
daemon: DocscribeDaemon,
): List<String> {
val sdk = ProjectRootManager.getInstance(project).projectSdk
val sdkInfo =
if (sdk != null) "${sdk.name} (home: ${sdk.homePath ?: "?"})" else "not configured"
val rubyPath = daemon.getRubyPath()
return listOf("IDE Ruby SDK: $sdkInfo", "Ruby binary: ${rubyPath ?: "not found"}")
}

/**
* docscribe gem availability, version, and server mode support.
*/
private fun gemInfoLines(
status: DocscribeStatus,
caps: DocscribeCapabilities?,
): List<String> {
val lines = mutableListOf("docscribe gem: ${statusLabel(status)}")
if (status == DocscribeStatus.UNCHECKED) {
lines += " (run any DocScribe action to trigger gem detection)"
}
if (caps != null) {
lines += " Version: ${caps.version}"
lines += " Server mode: ${if (caps.serverMode) "supported (>= 1.5.1)" else "not available (< 1.5.1)"}"
}
return lines
}

/**
* Daemon server state and fallback explanation.
*/
private fun daemonInfoLines(
project: Project,
status: DocscribeStatus,
caps: DocscribeCapabilities?,
): List<String> {
val daemon = DocscribeDaemon.getInstance(project)
val running = daemon.isServerRunning()
val lines = mutableListOf("Daemon server: ${if (running) "running" else "stopped"}")
if (status == DocscribeStatus.AVAILABLE) {
val shouldRun = caps?.serverMode == true
when {
shouldRun && !running -> lines += " (will start on next DocScribe action)"
!shouldRun -> lines += " (server not available — using CLI fallback)"
}
} else if (status == DocscribeStatus.MISSING) {
lines += " (gem not installed — see 'docscribe gem' section above)"
}
return lines
}

/**
* Collect identified issues with actionable steps.
*/
private fun issuesSummaryLines(
rubyPath: String?,
gemRoot: String?,
status: DocscribeStatus,
): List<String> {
val issues = mutableListOf<String>()
if (rubyPath == null) issues += "No Ruby SDK configured or found on PATH."
if (gemRoot == null) issues += "No Gemfile found in project tree."
if (status == DocscribeStatus.MISSING) {
issues += "docscribe gem is not installed. Add 'gem \"docscribe\"' to Gemfile and run 'bundle install'."
}
return if (issues.isEmpty()) {
listOf("Status: OK — all systems nominal.")
} else {
listOf("Issues found:") + issues.map { " - $it" }
}
}

/**
* Human-readable label for a [DocscribeStatus] value.
*/
private fun statusLabel(status: DocscribeStatus): String =
when (status) {
DocscribeStatus.UNCHECKED -> "not yet checked"
DocscribeStatus.AVAILABLE -> "AVAILABLE"
DocscribeStatus.MISSING -> "MISSING"
}

/**
* Show a DocScribe information notification.
*
* @param project The project to show the notification in.
* @param content The multi-line diagnostic report.
*/
private fun notify(
project: Project,
content: String,
) {
val group = NotificationGroupManager.getInstance().getNotificationGroup("DocScribe")
group.createNotification(content, NotificationType.INFORMATION).notify(project)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ class SafeFixAction : AnAction() {
*/
override fun update(e: AnActionEvent) {
val file = e.getData(CommonDataKeys.VIRTUAL_FILE)
e.presentation.isEnabledAndVisible = file != null && (file.name.endsWith(".rb") || file.name.endsWith(".rake"))
e.presentation.isEnabledAndVisible =
file != null && (file.name.endsWith(".rb") || file.name.endsWith(".rake") || file.name == "Rakefile")
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class DocscribeAggressiveFixIntention : IntentionAction {
project: Project,
editor: Editor?,
file: PsiFile?,
): Boolean = file != null && (file.name.endsWith(".rb") || file.name.endsWith(".rake"))
): Boolean = file != null && (file.name.endsWith(".rb") || file.name.endsWith(".rake") || file.name == "Rakefile")

/**
* Run docscribe aggressive fix in a background task, then refresh the file on success.
Expand Down
Loading
Loading