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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## [Unreleased]

- Rearchitected to use coroutines for asynchronous operations, hopefully improving CPU utilisation. Please report any issues to the github, as there should be no changes to the experience.

## 0.8.4 - 2025-06-25

- Fix issue where characters with special character encodings were causing the supplied range to the dprint daemon to
Expand Down
121 changes: 121 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build Commands

- **Build Plugin:** `./gradlew build`
- **Run Format:** `./gradlew ktlintFormat`
- **Run Tests:** `./gradlew check`
- **Run Verification:** `./gradlew runPluginVerifier`
- **Clean Build:** `./gradlew clean`

## Project Architecture
Comment thread
ryan-rushton marked this conversation as resolved.

The dprint-intellij project is an IntelliJ plugin that integrates the dprint code formatter (https://dprint.dev/) into
IntelliJ IDEs. The plugin allows users to format their code using dprint directly from the IDE.

### Key Components

1. **DprintService** - Central service that manages the dprint process and coordinates formatting requests
- Handles initialization of the appropriate editor service based on dprint schema version
- Manages a cache of files that can be formatted
- Maintains service state (`UNINITIALIZED`, `INITIALIZING`, `READY`, `ERROR`)
- Provides both callback and coroutine-based formatting APIs
- Thread-safe using `AtomicReference<ServiceStateData>`

2. **DprintTaskExecutor** - Handles background task execution and coordination
- Implements `CoroutineScope` for modern Kotlin coroutines
- Uses `Channel<QueuedTask>` for task queuing with deduplication
- Ensures only one task of each type runs at a time
- Integrates with IntelliJ's `ProgressManager` for UI feedback
- Handles timeouts and proper cancellation support

3. **EditorService Implementations**
- `EditorServiceV4` and `EditorServiceV5` - Implement different versions of the dprint editor service protocol
- `EditorServiceInitializer` - Detects schema version and creates appropriate service with improved error handling
- `EditorServiceCache` - LRU cache for `canFormat` results to improve performance
- Handle communication with the dprint CLI process with better config discovery and logging

4. **DprintExternalFormatter** - Integration with IntelliJ's external formatter API
- Determines if a file can be formatted by dprint
- Creates `DprintFormattingTask` instances for eligible files
- Integrates with `AsyncDocumentFormattingService`

5. **Configuration**
- `ProjectConfiguration` - Project-level settings (stored in .idea/dprintProjectConfig.xml)
- `UserConfiguration` - User-level settings (stored in .idea/dprintUserConfig.xml)
- `DprintConfigurable` - UI for configuring the plugin with reset functionality

6. **Actions**
- `ReformatAction` - Triggers dprint formatting
- `RestartAction` - Restarts the dprint editor service
- `ClearCacheAction` - Clears the format cache

### Data Flow

1. User action (manual format or save) triggers the formatter
2. `DprintExternalFormatter` is used by the internal IntelliJ formatter and keybinds if dprint can format the file
3. If eligible, a `DprintFormattingTask` is created and executed via `DprintService`
4. `DprintService` delegates the task to `DprintTaskExecutor` for background processing
5. The task is executed by the appropriate `EditorService` implementation (V4 or V5)
6. The dprint CLI daemon formats the file and returns the result
7. The formatted content is applied to the document

## Development Notes

1. The plugin is developed using Kotlin and targets IntelliJ 2024.3+
2. JDK 17 is required for development
3. To test the plugin locally:
- Install dprint CLI (`brew install dprint` on macOS)
- Run `dprint init` to create a default config
- Use the "Run IDE with Plugin" run configuration

4. Plugin requires a working dprint executable and config file. It can:
- Auto-detect dprint in PATH or node_modules
- Auto-detect config in project root
- Accept custom paths for both executable and config

5. The plugin supports both dprint schema v4 and v5

6. The plugin uses Kotlin coroutines for background task processing
- Uses `DprintTaskExecutor` with `Channel<QueuedTask>` for managing asynchronous operations
- Properly handles task cancellation, timeouts, and deduplication
- Integrates with IntelliJ's progress system for UI feedback

## Recent Improvements (v0.9.0)

### Major Architectural Refactoring
- **Coroutines migration** - Complete rearchitecture from callback-based to coroutine-based asynchronous operations for improved CPU utilization
- **New service architecture** - Introduced `DprintService` as central coordinator and `DprintTaskExecutor` for background task management using Kotlin coroutines
- **Editor service refactoring** - Split monolithic `EditorServiceManager` into focused components: `EditorServiceInitializer`, `EditorServiceCache`, and improved V4/V5 implementations
- **State management overhaul** - New `BaseConfiguration` abstraction for type-safe configuration management

### Range Formatting Enhancements
- **Range formatting re-implementation** - Added `DprintRangeFormattingTask` with improved character encoding handling
- **Fixed character encoding issues** - Resolved problems with special characters causing incorrect range calculations
- **Better integration** - Improved range formatting integration with IntelliJ's formatting system

### Performance & Reliability Improvements
- **Task queue optimization** - New `Channel<QueuedTask>` based system with deduplication and proper cancellation support
- **Caching improvements** - Enhanced `EditorServiceCache` with LRU caching for `canFormat` results
- **Timeout handling** - Better timeout management and error recovery throughout the plugin
- **Memory management** - Improved resource cleanup and lifecycle management

### Developer Experience Enhancements
- **Enhanced error handling** - Fixed JSON parsing errors with graceful handling for empty `dprint editor-info` output
- **Improved config discovery** - Better working directory handling and config file detection with user-friendly logging
- **Reset to defaults functionality** - Replaced restart button with comprehensive reset functionality covering all configuration settings
- **Better logging** - User-friendly messages showing which config files are being used and actionable error guidance
- **Progress integration** - Better integration with IntelliJ's progress system for background operations

### Technical Updates
- **Dependency updates** - Updated all dependencies for 2025, including Gradle foojay-resolver-convention plugin (0.7.0 → 1.0.0)
- **IntelliJ platform updates** - Updated to target IntelliJ 2024.3+ with latest platform APIs
- **Deprecated code removal** - Cleaned up deprecated class usage and modernized codebase
- **Test improvements** - Added comprehensive test suite for new architecture including `DprintServiceUnitTest`, `EditorServiceCacheTest`, and improved V5 service tests

### Configuration & UI Improvements
- **Configuration persistence** - Better state management with separate project and user configurations
- **Bundle message improvements** - Enhanced localized messages for better user experience
- **Git ignore updates** - Improved `.gitignore` for better development workflow
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# dprint

<!-- Plugin description -->
This plugin adds support for dprint, a flexible and extensible code formatter ([dprint.dev](https://dprint.dev/)). It is
in active early development, please report bugs and feature requests to
our [github](https://github.com/dprint/dprint-intellij/issues).
This plugin adds support for dprint, a flexible and extensible code formatter ([dprint.dev](https://dprint.dev/)).
Please report bugs and feature requests to our [github](https://github.com/dprint/dprint-intellij/issues).

N.B. Currently only UTF-8 file formats are supported correctly.

Expand All @@ -26,8 +25,11 @@ To use this plugin:
that may be stopping your file from being formatted.

This plugin uses a long-running process known as the `editor-service`. If you change your `dprint.json` file outside of
IntelliJ or dprint is not formatting as expected, run the `Restart dprint` action or in `Preferences` -> `Tools` ->
`dprint` click the `Restart` button. This will force the editor service to close down and restart.
IntelliJ or dprint is not formatting as expected, run the `Restart dprint` action. If you need to reset all settings
to defaults, use the `Reset to Defaults` button in `Preferences` -> `Tools` -> `dprint`. This will reset all
configuration values and restart the editor service.

The plugin supports both dprint schema v4 and v5 and will automatically detect the appropriate version based on your dprint installation.

Please report any issues with this Intellij plugin to the
[github repository](https://github.com/dprint/dprint-intellij/issues).
Expand All @@ -45,8 +47,8 @@ Please report any issues with this Intellij plugin to the

## Development

This project is currently built using JDK 17. To install on a mac with homebrew run `brew install java` and set that
be your project SDK.
This project is currently built using JDK 17 and targets IntelliJ 2024.3+. To install on a mac with homebrew run `brew install openjdk@17` and set that
as your project SDK.

### Dprint setup

Expand All @@ -55,9 +57,8 @@ When running the plugin via the `Run Plugin` configuration, add a default dprint

### Intellij Setup

- Set up linting settings, run <kbd>Gradle</kbd> > <kbd>Tasks</kbd> > <kbd>help</kbd> > <kbd>
ktlintGernateBaseline</kbd>.
This sets up intellij with appropriate formatting settings.
- Set up linting settings, run <kbd>Gradle</kbd> > <kbd>Tasks</kbd> > <kbd>help</kbd> > <kbd>ktlintGenerateBaseline</kbd>.
This sets up IntelliJ with appropriate formatting settings.

### Running

Expand All @@ -67,5 +68,14 @@ There are 3 default run configs set up
- <kbd>Run Tests</kbd> - This runs linting and tests.
- <kbd>Run Verifications</kbd> - This verifies the plugin is publishable.

Depending on the version of IntellJ you are running for development, you will need to change the `platformType` property
Depending on the version of IntelliJ you are running for development, you will need to change the `platformType` property
in `gradle.properties`. It is IU for IntelliJ Ultimate and IC for IntelliJ Community.

### Plugin Architecture

The plugin uses a simplified architecture centered around:
- **DprintService**: Central service managing formatting operations and state
- **DprintTaskExecutor**: Handles background task execution using Kotlin coroutines
- **EditorService**: Communicates with the dprint CLI daemon (supports v4 and v5 schemas)
- **DprintExternalFormatter**: Integrates with IntelliJ's formatting system
- **Configuration**: Project and user-level settings with reset functionality
4 changes: 2 additions & 2 deletions src/main/kotlin/com/dprint/actions/ClearCacheAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.dprint.actions

import com.dprint.config.ProjectConfiguration
import com.dprint.i18n.DprintBundle
import com.dprint.services.editorservice.EditorServiceManager
import com.dprint.services.DprintService
import com.dprint.utils.infoLogWithConsole
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
Expand All @@ -20,7 +20,7 @@ class ClearCacheAction : AnAction() {
val projectConfig = it.service<ProjectConfiguration>().state
if (!projectConfig.enabled) return@let
infoLogWithConsole(DprintBundle.message("clear.cache.action.run"), it, LOGGER)
it.service<EditorServiceManager>().clearCanFormatCache()
it.service<DprintService>().clearCanFormatCache()
}
}
}
4 changes: 2 additions & 2 deletions src/main/kotlin/com/dprint/actions/RestartAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.dprint.actions

import com.dprint.config.ProjectConfiguration
import com.dprint.i18n.DprintBundle
import com.dprint.services.editorservice.EditorServiceManager
import com.dprint.services.DprintService
import com.dprint.utils.infoLogWithConsole
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
Expand All @@ -20,7 +20,7 @@ class RestartAction : AnAction() {
val enabled = it.service<ProjectConfiguration>().state.enabled
if (!enabled) return@let
infoLogWithConsole(DprintBundle.message("restart.action.run"), it, LOGGER)
it.service<EditorServiceManager>().restartEditorService()
it.service<DprintService>().restartEditorService()
}
}
}
4 changes: 2 additions & 2 deletions src/main/kotlin/com/dprint/config/DprintConfigurable.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.dprint.config

import com.dprint.i18n.DprintBundle
import com.dprint.services.editorservice.EditorServiceManager
import com.dprint.services.DprintService
import com.dprint.utils.validateConfigFile
import com.dprint.utils.validateExecutablePath
import com.intellij.ide.actionsOnSave.ActionOnSaveBackedByOwnConfigurable
Expand Down Expand Up @@ -34,7 +34,7 @@ class DprintConfigurable(private val project: Project) : BoundSearchableConfigur
override fun createPanel(): DialogPanel {
val projectConfig = project.service<ProjectConfiguration>()
val userConfig = project.service<UserConfiguration>()
val dprintService = project.service<EditorServiceManager>()
val dprintService = project.service<DprintService>()

return panel {
// Restart or destroy editor service on apply
Expand Down
32 changes: 26 additions & 6 deletions src/main/kotlin/com/dprint/formatter/DprintExternalFormatter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.dprint.formatter
import com.dprint.config.ProjectConfiguration
import com.dprint.config.UserConfiguration
import com.dprint.i18n.DprintBundle
import com.dprint.services.editorservice.EditorServiceManager
import com.dprint.services.DprintService
import com.dprint.utils.infoConsole
import com.dprint.utils.infoLogWithConsole
import com.dprint.utils.isFormattableFile
Expand Down Expand Up @@ -36,7 +36,7 @@ class DprintExternalFormatter : AsyncDocumentFormattingService() {
override fun canFormat(file: PsiFile): Boolean {
val projectConfig = file.project.service<ProjectConfiguration>().state
val userConfig = file.project.service<UserConfiguration>().state
val editorServiceManager = file.project.service<EditorServiceManager>()
val dprintService = file.project.service<DprintService>()

if (!projectConfig.enabled) return false

Expand All @@ -52,7 +52,7 @@ class DprintExternalFormatter : AsyncDocumentFormattingService() {
val virtualFile = file.virtualFile ?: file.originalFile.virtualFile
val canFormat =
if (virtualFile != null && isFormattableFile(file.project, virtualFile)) {
editorServiceManager.canFormatCached(virtualFile.path)
dprintService.canFormatCached(virtualFile.path)
} else {
false
}
Expand All @@ -75,20 +75,40 @@ class DprintExternalFormatter : AsyncDocumentFormattingService() {
override fun createFormattingTask(formattingRequest: AsyncFormattingRequest): FormattingTask? {
val project = formattingRequest.context.project

val editorServiceManager = project.service<EditorServiceManager>()
val dprintService = project.service<DprintService>()
val path = formattingRequest.ioFile?.path

if (path == null) {
infoLogWithConsole(DprintBundle.message("formatting.cannot.determine.file.path"), project, LOGGER)
return null
}

if (!editorServiceManager.canRangeFormat() && isRangeFormat(formattingRequest)) {
if (!dprintService.canRangeFormat() && isRangeFormat(formattingRequest)) {
// When range formatting is available we need to add support here.
infoLogWithConsole(DprintBundle.message("external.formatter.range.formatting"), project, LOGGER)
return null
}

if (dprintService.canRangeFormat() && isRangeFormat(formattingRequest)) {
infoLogWithConsole(DprintBundle.message("external.formatter.range.formatting"), project, LOGGER)

return object : FormattingTask {
val dprintTask = DprintRangeFormattingTask(project, dprintService, formattingRequest, path)

override fun run() {
return dprintTask.run()
}

override fun cancel(): Boolean {
return dprintTask.cancel()
}

override fun isRunUnderProgress(): Boolean {
return dprintTask.isRunUnderProgress()
}
}
}

if (doAnyRangesIntersect(formattingRequest)) {
infoLogWithConsole(DprintBundle.message("external.formatter.range.overlapping"), project, LOGGER)
return null
Expand All @@ -97,7 +117,7 @@ class DprintExternalFormatter : AsyncDocumentFormattingService() {
infoLogWithConsole(DprintBundle.message("external.formatter.creating.task", path), project, LOGGER)

return object : FormattingTask {
val dprintTask = DprintFormattingTask(project, editorServiceManager, formattingRequest, path)
val dprintTask = DprintFormattingTask(project, dprintService, formattingRequest, path)

override fun run() {
return dprintTask.run()
Expand Down
Loading