Skip to content

Commit 22af1c2

Browse files
committed
feat(lsp): trim single trailing spaces on type
1 parent 7e395b6 commit 22af1c2

File tree

8 files changed

+182
-2
lines changed

8 files changed

+182
-2
lines changed

quarkdown-lsp/src/main/kotlin/com/quarkdown/lsp/QuarkdownLanguageServer.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import com.quarkdown.lsp.diagnostics.DiagnosticsSuppliersFactory
66
import com.quarkdown.lsp.highlight.SemanticTokensSuppliersFactory
77
import com.quarkdown.lsp.highlight.TokenType
88
import com.quarkdown.lsp.hover.HoverSuppliersFactory
9+
import com.quarkdown.lsp.ontype.OnTypeFormattingSuppliersFactory
910
import com.quarkdown.lsp.pattern.QuarkdownPatterns
1011
import org.eclipse.lsp4j.CompletionOptions
1112
import org.eclipse.lsp4j.Diagnostic
13+
import org.eclipse.lsp4j.DocumentOnTypeFormattingOptions
1214
import org.eclipse.lsp4j.InitializeParams
1315
import org.eclipse.lsp4j.InitializeResult
1416
import org.eclipse.lsp4j.MessageParams
@@ -44,6 +46,7 @@ class QuarkdownLanguageServer(
4446
SemanticTokensSuppliersFactory.default(),
4547
HoverSuppliersFactory.default(this),
4648
DiagnosticsSuppliersFactory.default(this),
49+
OnTypeFormattingSuppliersFactory.default(),
4750
)
4851

4952
private val completionTriggers =
@@ -55,6 +58,8 @@ class QuarkdownLanguageServer(
5558
)
5659
}
5760

61+
private val onTypeFormattingOptions = DocumentOnTypeFormattingOptions("\n")
62+
5863
private val workspaceService: WorkspaceService = QuarkdownWorkspaceService(this)
5964

6065
private lateinit var client: LanguageClient
@@ -85,6 +90,7 @@ class QuarkdownLanguageServer(
8590
completionProvider = CompletionOptions(true, completionTriggers)
8691
hoverProvider = Either.forLeft(true)
8792
semanticTokensProvider = SemanticTokensWithRegistrationOptions(legend, true, null)
93+
documentOnTypeFormattingProvider = onTypeFormattingOptions
8894
}
8995
val response = InitializeResult(serverCaps)
9096

quarkdown-lsp/src/main/kotlin/com/quarkdown/lsp/QuarkdownTextDocumentService.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import com.quarkdown.lsp.completion.CompletionSupplier
55
import com.quarkdown.lsp.diagnostics.DiagnosticsSupplier
66
import com.quarkdown.lsp.highlight.SemanticTokensSupplier
77
import com.quarkdown.lsp.hover.HoverSupplier
8+
import com.quarkdown.lsp.ontype.OnTypeFormattingEditSupplier
89
import com.quarkdown.lsp.subservices.CompletionSubservice
910
import com.quarkdown.lsp.subservices.DiagnosticsSubservice
1011
import com.quarkdown.lsp.subservices.HoverSubservice
12+
import com.quarkdown.lsp.subservices.OnTypeFormattingSubservice
1113
import com.quarkdown.lsp.subservices.SemanticTokensSubservice
1214
import org.eclipse.lsp4j.CompletionItem
1315
import org.eclipse.lsp4j.CompletionList
@@ -17,11 +19,13 @@ import org.eclipse.lsp4j.DidChangeTextDocumentParams
1719
import org.eclipse.lsp4j.DidCloseTextDocumentParams
1820
import org.eclipse.lsp4j.DidOpenTextDocumentParams
1921
import org.eclipse.lsp4j.DidSaveTextDocumentParams
22+
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams
2023
import org.eclipse.lsp4j.Hover
2124
import org.eclipse.lsp4j.HoverParams
2225
import org.eclipse.lsp4j.SemanticTokens
2326
import org.eclipse.lsp4j.SemanticTokensParams
2427
import org.eclipse.lsp4j.TextDocumentIdentifier
28+
import org.eclipse.lsp4j.TextEdit
2529
import org.eclipse.lsp4j.jsonrpc.messages.Either
2630
import org.eclipse.lsp4j.services.TextDocumentService
2731
import java.util.concurrent.CompletableFuture
@@ -38,11 +42,13 @@ class QuarkdownTextDocumentService(
3842
tokensSuppliers: List<SemanticTokensSupplier>,
3943
hoverSuppliers: List<HoverSupplier>,
4044
diagnosticsSuppliers: List<DiagnosticsSupplier>,
45+
formattingSuppliers: List<OnTypeFormattingEditSupplier>,
4146
) : TextDocumentService {
4247
private val completionService = CompletionSubservice(completionSuppliers)
4348
private val semanticTokensService = SemanticTokensSubservice(tokensSuppliers)
4449
private val hoverService = HoverSubservice(hoverSuppliers)
4550
private val diagnosticsService = DiagnosticsSubservice(diagnosticsSuppliers)
51+
private val onTypeFormattingService = OnTypeFormattingSubservice(formattingSuppliers)
4652

4753
/**
4854
* Maps document URIs to their text content.
@@ -155,4 +161,11 @@ class QuarkdownTextDocumentService(
155161
val document = getDocument(params.textDocument)
156162
return CompletableFuture.completedFuture(hoverService.process(params, document))
157163
}
164+
165+
override fun onTypeFormatting(params: DocumentOnTypeFormattingParams): CompletableFuture<List<TextEdit?>?>? {
166+
server.log("Operation 'text/onTypeFormatting'")
167+
168+
val document = getDocument(params.textDocument)
169+
return CompletableFuture.completedFuture(onTypeFormattingService.process(params, document))
170+
}
158171
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.quarkdown.lsp.ontype
2+
3+
import com.quarkdown.lsp.TextDocument
4+
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams
5+
import org.eclipse.lsp4j.TextEdit
6+
7+
/**
8+
* Supplier of text edits for on-type formatting.
9+
*/
10+
interface OnTypeFormattingEditSupplier {
11+
/**
12+
* Provides text edits for on-type formatting based on the given parameters and document.
13+
* @param params the parameters for the on-type formatting request
14+
* @param document the text document to format
15+
* @return a list of text edits to apply to the document
16+
*/
17+
fun getEdits(
18+
params: DocumentOnTypeFormattingParams,
19+
document: TextDocument,
20+
): List<TextEdit>
21+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.quarkdown.lsp.ontype
2+
3+
/**
4+
* Factory for creating [OnTypeFormattingEditSupplier]s.
5+
*/
6+
object OnTypeFormattingSuppliersFactory {
7+
fun default(): List<OnTypeFormattingEditSupplier> = listOf(TrailingSpacesRemoverOnTypeFormattingEditSupplier())
8+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.quarkdown.lsp.ontype
2+
3+
import com.quarkdown.lsp.TextDocument
4+
import com.quarkdown.lsp.util.getLine
5+
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams
6+
import org.eclipse.lsp4j.Position
7+
import org.eclipse.lsp4j.Range
8+
import org.eclipse.lsp4j.TextEdit
9+
10+
private const val TO_REMOVE = " "
11+
12+
/**
13+
* Formatter that removes a single trailing space at the end of the previous line when the user types a newline,
14+
* but keeps double (or more) spaces which are significant in Markdown for hard line breaks.
15+
*/
16+
class TrailingSpacesRemoverOnTypeFormattingEditSupplier : OnTypeFormattingEditSupplier {
17+
override fun getEdits(
18+
params: DocumentOnTypeFormattingParams,
19+
document: TextDocument,
20+
): List<TextEdit> {
21+
val lineNum = params.position.line - 1 // Line before the newline.
22+
val line =
23+
document.text.getLine(lineNum)
24+
?: return emptyList() // No such line.
25+
26+
if (!line.endsWith(TO_REMOVE)) return emptyList() // No trailing space.
27+
if (line.endsWith(TO_REMOVE + TO_REMOVE)) return emptyList() // More than one trailing space.
28+
29+
val edit =
30+
TextEdit(
31+
Range(
32+
Position(lineNum, line.length - TO_REMOVE.length),
33+
Position(lineNum, line.length),
34+
),
35+
"",
36+
)
37+
38+
return listOf(edit)
39+
}
40+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.quarkdown.lsp.subservices
2+
3+
import com.quarkdown.lsp.TextDocument
4+
import com.quarkdown.lsp.ontype.OnTypeFormattingEditSupplier
5+
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams
6+
import org.eclipse.lsp4j.TextEdit
7+
8+
/**
9+
* Subservice for handling on-type formatting requests.
10+
* It aggregates edits from all suppliers and returns them as a single list.
11+
*/
12+
class OnTypeFormattingSubservice(
13+
private val editSuppliers: List<OnTypeFormattingEditSupplier>,
14+
) : TextDocumentSubservice<DocumentOnTypeFormattingParams, List<TextEdit>> {
15+
override fun process(
16+
params: DocumentOnTypeFormattingParams,
17+
document: TextDocument,
18+
): List<TextEdit> = editSuppliers.flatMap { it.getEdits(params, document) }
19+
}

quarkdown-lsp/src/main/kotlin/com/quarkdown/lsp/util/PositionUtils.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,23 @@ package com.quarkdown.lsp.util
33
import com.quarkdown.core.util.substringWithinBounds
44
import org.eclipse.lsp4j.Position
55

6+
/**
7+
* @param line the line number (0-based)
8+
* @return the line at the specified index, or null if the index is out of bounds
9+
*/
10+
fun String.getLine(line: Int): String? = lines().getOrNull(line)
11+
612
/**
713
* @param text the text content to search in
814
* @return the character at the specified position, or null if the position is out of bounds
915
*/
10-
fun Position.getChar(text: String): Char? = text.lines().getOrNull(line)?.getOrNull(character - 1)
16+
fun Position.getChar(text: String): Char? = text.getLine(line)?.getOrNull(character - 1)
1117

1218
/**
1319
* @param text the text content to search in
1420
* @return the substring from the start of the line, up to the specified position, or `null` if the position is out of bounds
1521
*/
16-
fun Position.getLineUntilPosition(text: String): String? = text.lines()[line].substringWithinBounds(0, character)
22+
fun Position.getLineUntilPosition(text: String): String? = text.getLine(line)?.substringWithinBounds(0, character)
1723

1824
/**
1925
* @param text the text content to search in
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.quarkdown.lsp
2+
3+
import com.quarkdown.core.util.normalizeLineSeparators
4+
import com.quarkdown.lsp.ontype.TrailingSpacesRemoverOnTypeFormattingEditSupplier
5+
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams
6+
import org.eclipse.lsp4j.FormattingOptions
7+
import org.eclipse.lsp4j.Position
8+
import org.eclipse.lsp4j.TextDocumentIdentifier
9+
import org.eclipse.lsp4j.TextEdit
10+
import kotlin.test.Test
11+
import kotlin.test.assertEquals
12+
13+
/**
14+
* Tests for [TrailingSpacesRemoverOnTypeFormattingEditSupplier].
15+
*/
16+
class TrailingSpacesRemoverFormattingEditSupplierTest {
17+
private val supplier = TrailingSpacesRemoverOnTypeFormattingEditSupplier()
18+
19+
private fun getEdits(
20+
text: String,
21+
atLine: Int,
22+
): List<TextEdit> {
23+
val doc = TextDocument(text.normalizeLineSeparators().toString())
24+
val options = FormattingOptions(2, true)
25+
val params =
26+
DocumentOnTypeFormattingParams(TextDocumentIdentifier("mem://test.md"), options, Position(atLine, 0), "\n")
27+
return supplier.getEdits(params, doc)
28+
}
29+
30+
@Test
31+
fun `removes single trailing space`() {
32+
val text = "Hello \n"
33+
val edits = getEdits(text, 1)
34+
assertEquals(1, edits.size)
35+
assertEquals(
36+
5,
37+
edits
38+
.single()
39+
.range.start.character,
40+
)
41+
assertEquals(
42+
6,
43+
edits
44+
.single()
45+
.range.end.character,
46+
)
47+
}
48+
49+
@Test
50+
fun `removes single trailing spaces among multiple lines`() {
51+
val text = "Hello \nWorld \nThis is a test \n"
52+
val edits = getEdits(text, 3)
53+
assertEquals(1, edits.size)
54+
}
55+
56+
@Test
57+
fun `keeps double trailing space`() {
58+
val text = "Hello \n"
59+
assertEquals(0, getEdits(text, 1).size)
60+
}
61+
62+
@Test
63+
fun `no trailing space to remove`() {
64+
val text = "Hello\n"
65+
assertEquals(0, getEdits(text, 1).size)
66+
}
67+
}

0 commit comments

Comments
 (0)