Skip to content

Commit e71dad1

Browse files
authored
Fix formatting when text includes a special char (#112)
* Fix formatting when text includes a special char * Release docs * Remove claude permissions
1 parent 491b433 commit e71dad1

5 files changed

Lines changed: 153 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
## [Unreleased]
66

7+
- Fix issue where characters with special character encodings were causing the supplied range to the dprint daemon to
8+
be incorrect. This is a side effect of https://github.com/dprint/dprint-intellij/pull/107 as IJ now sends a full file
9+
range when the internal IJ formatter is overridden.
10+
711
## 0.8.3 - 2025-06-07
812

913
- While range formatting is not currently available, allow format fragment flows to come through so logging occurs.

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ This plugin adds support for dprint, a flexible and extensible code formatter ([
55
in active early development, please report bugs and feature requests to
66
our [github](https://github.com/dprint/dprint-intellij/issues).
77

8+
N.B. Currently only UTF-8 file formats are supported correctly.
9+
810
To use this plugin:
911

1012
- Install and configure dprint for your repository, [dprint.dev/setup](https://dprint.dev/setup/)
@@ -48,12 +50,13 @@ be your project SDK.
4850

4951
### Dprint setup
5052

51-
To test this plugin, you will require dprint installed. To install on a mac with homebrew run `brew install dprint`.
53+
To test this plugin, you will require dprint installed. To install on a mac with homebrew run `brew install dprint`.
5254
When running the plugin via the `Run Plugin` configuration, add a default dprint config file by running `dprint init`.
5355

5456
### Intellij Setup
5557

56-
- Set up linting settings, run <kbd>Gradle</kbd> > <kbd>Tasks</kbd> > <kbd>help</kbd> > <kbd>ktlintGernateBaseline</kbd>.
58+
- Set up linting settings, run <kbd>Gradle</kbd> > <kbd>Tasks</kbd> > <kbd>help</kbd> > <kbd>
59+
ktlintGernateBaseline</kbd>.
5760
This sets up intellij with appropriate formatting settings.
5861

5962
### Running

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html
33
pluginGroup=com.dprint.intellij.plugin
44
pluginName=dprint
5-
pluginVersion=0.8.3
5+
pluginVersion=0.8.4
66
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
77
# for insight into build numbers and IntelliJ Platform versions.
88
pluginSinceBuild=241

src/main/kotlin/com/dprint/services/editorservice/v5/EditorServiceV5.kt

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ class EditorServiceV5Impl(
149149
(result.type == MessageType.CanFormatResponse && result.data is Boolean) -> {
150150
onFinished(result.data)
151151
}
152+
152153
(result.type == MessageType.ErrorResponse && result.data is String) -> {
153154
infoLogWithConsole(
154155
DprintBundle.message("editor.service.format.check.failed", filePath, result.data),
@@ -157,10 +158,12 @@ class EditorServiceV5Impl(
157158
)
158159
onFinished(null)
159160
}
161+
160162
(result.type === MessageType.Dropped) -> {
161163
// do nothing
162164
onFinished(null)
163165
}
166+
164167
else -> {
165168
infoLogWithConsole(
166169
DprintBundle.message("editor.service.unsupported.message.type", result.type),
@@ -225,14 +228,41 @@ class EditorServiceV5Impl(
225228
): OutgoingMessage {
226229
val outgoingMessage = OutgoingMessage(formatId ?: getNextMessageId(), MessageType.FormatFile)
227230
outgoingMessage.addString(filePath)
228-
// TODO We need to properly handle string index to byte index here
229-
outgoingMessage.addInt(startIndex ?: 0) // for range formatting add starting index
230-
outgoingMessage.addInt(endIndex ?: content.encodeToByteArray().size) // add ending index
231+
232+
// Converting string indices to bytes
233+
val startByteIndex =
234+
if (startIndex != null) {
235+
getByteIndex(content, startIndex)
236+
} else {
237+
0
238+
}
239+
240+
val endByteIndex =
241+
if (endIndex != null) {
242+
getByteIndex(content, endIndex)
243+
} else {
244+
content.encodeToByteArray().size
245+
}
246+
247+
outgoingMessage.addInt(startByteIndex) // for range formatting add starting index
248+
outgoingMessage.addInt(endByteIndex) // add ending index
231249
outgoingMessage.addInt(0) // Override config
232250
outgoingMessage.addString(content)
233251
return outgoingMessage
234252
}
235253

254+
private fun getByteIndex(
255+
content: String,
256+
stringIndex: Int,
257+
): Int {
258+
// Handle edge cases
259+
if (stringIndex <= 0) return 0
260+
if (stringIndex >= content.length) return content.encodeToByteArray().size
261+
262+
// Get substring up to the string index and convert to bytes
263+
return content.substring(0, stringIndex).encodeToByteArray().size
264+
}
265+
236266
private fun mapResultToFormatResult(
237267
result: PendingMessages.Result,
238268
filePath: String,
@@ -247,6 +277,7 @@ class EditorServiceV5Impl(
247277
infoLogWithConsole(successMessage, project, LOGGER)
248278
FormatResult(formattedContent = result.data)
249279
}
280+
250281
(result.type == MessageType.ErrorResponse && result.data is String) -> {
251282
warnLogWithConsole(
252283
DprintBundle.message("editor.service.format.failed", filePath, result.data),
@@ -255,6 +286,7 @@ class EditorServiceV5Impl(
255286
)
256287
FormatResult(error = result.data)
257288
}
289+
258290
(result.type != MessageType.Dropped) -> {
259291
val errorMessage = DprintBundle.message("editor.service.unsupported.message.type", result.type)
260292
warnLogWithConsole(
@@ -263,7 +295,9 @@ class EditorServiceV5Impl(
263295
LOGGER,
264296
)
265297
FormatResult(error = errorMessage)
266-
} else -> {
298+
}
299+
300+
else -> {
267301
FormatResult()
268302
}
269303
}

src/test/kotlin/com/dprint/services/editorservice/v5/EditorServiceV5ImplTest.kt

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,109 @@ class EditorServiceV5ImplTest : FunSpec({
162162
verify { pendingMessages.take(testId) }
163163
verify { editorProcess.writeBuffer(expectedOutgoingMessage.build()) }
164164
}
165+
166+
test("fmt converts string indices to byte indices correctly for ASCII text") {
167+
val testFile = "/test/File.kt"
168+
val testContent = "hello world"
169+
val onFinished = mockk<(FormatResult) -> Unit>()
170+
171+
every { editorProcess.writeBuffer(any()) } returns Unit
172+
every { pendingMessages.store(any(), any()) } returns Unit
173+
every { onFinished(any()) } returns Unit
174+
175+
editorServiceV5.fmt(1, testFile, testContent, 6, 11, onFinished)
176+
177+
val expectedOutgoingMessage = OutgoingMessage(1, MessageType.FormatFile)
178+
expectedOutgoingMessage.addString(testFile)
179+
expectedOutgoingMessage.addInt(6) // "hello " = 6 bytes
180+
expectedOutgoingMessage.addInt(11) // "hello world" = 11 bytes
181+
expectedOutgoingMessage.addInt(0)
182+
expectedOutgoingMessage.addString(testContent)
183+
184+
verify(exactly = 1) { editorProcess.writeBuffer(expectedOutgoingMessage.build()) }
185+
}
186+
187+
test("fmt converts string indices to byte indices correctly for Unicode text") {
188+
val testFile = "/test/File.kt"
189+
val testContent = "🚀 rocket"
190+
val onFinished = mockk<(FormatResult) -> Unit>()
191+
192+
every { editorProcess.writeBuffer(any()) } returns Unit
193+
every { pendingMessages.store(any(), any()) } returns Unit
194+
every { onFinished(any()) } returns Unit
195+
196+
editorServiceV5.fmt(1, testFile, testContent, 2, 8, onFinished)
197+
198+
val expectedOutgoingMessage = OutgoingMessage(1, MessageType.FormatFile)
199+
expectedOutgoingMessage.addString(testFile)
200+
expectedOutgoingMessage.addInt(4) // 🚀 is 2 bytes then another 2 for ' r'
201+
expectedOutgoingMessage.addInt(10)
202+
expectedOutgoingMessage.addInt(0)
203+
expectedOutgoingMessage.addString(testContent)
204+
205+
verify(exactly = 1) { editorProcess.writeBuffer(expectedOutgoingMessage.build()) }
206+
}
207+
208+
test("fmt handles edge case with start index 0") {
209+
val testFile = "/test/File.kt"
210+
val testContent = "test"
211+
val onFinished = mockk<(FormatResult) -> Unit>()
212+
213+
every { editorProcess.writeBuffer(any()) } returns Unit
214+
every { pendingMessages.store(any(), any()) } returns Unit
215+
every { onFinished(any()) } returns Unit
216+
217+
editorServiceV5.fmt(1, testFile, testContent, 0, 2, onFinished)
218+
219+
val expectedOutgoingMessage = OutgoingMessage(1, MessageType.FormatFile)
220+
expectedOutgoingMessage.addString(testFile)
221+
expectedOutgoingMessage.addInt(0) // start at 0
222+
expectedOutgoingMessage.addInt(2) // "te" = 2 bytes
223+
expectedOutgoingMessage.addInt(0)
224+
expectedOutgoingMessage.addString(testContent)
225+
226+
verify(exactly = 1) { editorProcess.writeBuffer(expectedOutgoingMessage.build()) }
227+
}
228+
229+
test("fmt handles edge case with index beyond content length") {
230+
val testFile = "/test/File.kt"
231+
val testContent = "test"
232+
val onFinished = mockk<(FormatResult) -> Unit>()
233+
234+
every { editorProcess.writeBuffer(any()) } returns Unit
235+
every { pendingMessages.store(any(), any()) } returns Unit
236+
every { onFinished(any()) } returns Unit
237+
238+
editorServiceV5.fmt(1, testFile, testContent, 10, 20, onFinished)
239+
240+
val expectedOutgoingMessage = OutgoingMessage(1, MessageType.FormatFile)
241+
expectedOutgoingMessage.addString(testFile)
242+
expectedOutgoingMessage.addInt(4) // beyond length returns full content byte size
243+
expectedOutgoingMessage.addInt(4) // beyond length returns full content byte size
244+
expectedOutgoingMessage.addInt(0)
245+
expectedOutgoingMessage.addString(testContent)
246+
247+
verify(exactly = 1) { editorProcess.writeBuffer(expectedOutgoingMessage.build()) }
248+
}
249+
250+
test("fmt handles mixed Unicode characters correctly") {
251+
val testFile = "/test/File.kt"
252+
val testContent = "café 🎉 test"
253+
val onFinished = mockk<(FormatResult) -> Unit>()
254+
255+
every { editorProcess.writeBuffer(any()) } returns Unit
256+
every { pendingMessages.store(any(), any()) } returns Unit
257+
every { onFinished(any()) } returns Unit
258+
259+
editorServiceV5.fmt(1, testFile, testContent, 5, 7, onFinished)
260+
261+
val expectedOutgoingMessage = OutgoingMessage(1, MessageType.FormatFile)
262+
expectedOutgoingMessage.addString(testFile)
263+
expectedOutgoingMessage.addInt(testContent.substring(0, 5).encodeToByteArray().size) // Dynamic calculation
264+
expectedOutgoingMessage.addInt(testContent.substring(0, 7).encodeToByteArray().size) // Dynamic calculation
265+
expectedOutgoingMessage.addInt(0)
266+
expectedOutgoingMessage.addString(testContent)
267+
268+
verify(exactly = 1) { editorProcess.writeBuffer(expectedOutgoingMessage.build()) }
269+
}
165270
})

0 commit comments

Comments
 (0)