@@ -2,14 +2,20 @@ package com.company.plugin.completion
22
33import com.company.plugin.lsp.ZyLspService
44import com.intellij.codeInsight.completion.*
5+ import com.intellij.codeInsight.lookup.LookupElement
56import com.intellij.codeInsight.lookup.LookupElementBuilder
67import com.intellij.openapi.diagnostic.Logger
78import com.intellij.openapi.vfs.VirtualFile
89import com.intellij.patterns.PlatformPatterns
910import com.intellij.psi.PsiElement
1011import com.intellij.util.ProcessingContext
1112import org.eclipse.lsp4j.CompletionItem
13+ import org.eclipse.lsp4j.CompletionItemKind
1214import org.eclipse.lsp4j.CompletionList
15+ import org.eclipse.lsp4j.InsertTextFormat
16+ import org.eclipse.lsp4j.TextEdit
17+ import org.eclipse.lsp4j.Range
18+ import org.eclipse.lsp4j.Position
1319
1420/* *
1521 * ZY 代码补全贡献者
@@ -71,20 +77,20 @@ class ZyCompletionContributor : CompletionContributor() {
7177
7278 // 等待 LSP 响应(设置超时)
7379 try {
74- val completionResult = completionFuture.get(1 , java.util.concurrent.TimeUnit .SECONDS )
80+ val completionResult = completionFuture.get(3 , java.util.concurrent.TimeUnit .SECONDS )
7581
7682 if (completionResult.isLeft) {
7783 // List<CompletionItem>
7884 val items = completionResult.left
7985 for (item in items) {
80- result.addElement(createLookupElement(item))
86+ result.addElement(createLookupElement(item, document, offset ))
8187 }
8288 LOG .debug(" Added ${items.size} completion items" )
8389 } else {
8490 // CompletionList
8591 val completionList = completionResult.right
8692 for (item in completionList.items) {
87- result.addElement(createLookupElement(item))
93+ result.addElement(createLookupElement(item, document, offset ))
8894 }
8995 LOG .debug(" Added ${completionList.items.size} completion items from list" )
9096 }
@@ -100,47 +106,234 @@ class ZyCompletionContributor : CompletionContributor() {
100106 /* *
101107 * 将 LSP CompletionItem 转换为 IntelliJ LookupElement
102108 */
103- private fun createLookupElement (item : CompletionItem ): LookupElementBuilder {
104- var builder = LookupElementBuilder .create(item.insertText ? : item.label)
109+ private fun createLookupElement (item : CompletionItem , document : com.intellij.openapi.editor.Document , offset : Int ): LookupElement {
110+ // 使用 insertText 或 label 作为补全文本
111+ val lookupString = item.insertText ? : item.label
112+
113+ var builder = LookupElementBuilder .create(lookupString)
105114 .withPresentableText(item.label)
106115
107116 // 添加类型信息
108117 item.detail?.let { detail ->
109118 builder = builder.withTypeText(detail, true )
110119 }
111120
112- // 添加尾部文本
121+ // 添加文档信息作为尾部文本
113122 item.documentation?.let { doc ->
114- if (doc.isLeft && doc.left is String ) {
115- val docString = doc.left as String
116- if (docString.length <= 50 ) {
117- builder = builder.withTailText(" - $docString " , true )
123+ when {
124+ doc.isLeft && doc.left is String -> {
125+ val docString = doc.left as String
126+ if (docString.isNotEmpty()) {
127+ builder = builder.withTailText(" $docString " , true )
128+ }
129+ }
130+ doc.isRight -> {
131+ // 处理 MarkupContent 类型的文档
132+ val markupContent = doc.right
133+ if (markupContent != null && markupContent.value.isNotEmpty()) {
134+ val preview = if (markupContent.value.length > 50 ) {
135+ markupContent.value.substring(0 , 47 ) + " ..."
136+ } else {
137+ markupContent.value
138+ }
139+ builder = builder.withTailText(" $preview " , true )
140+ }
118141 }
119142 }
120143 }
121144
122145 // 设置图标(根据 CompletionItemKind)
123- item.kind?.let { kind ->
124- builder = builder.withIcon(getIconForKind(kind))
146+ val icon = getIconForKind(item.kind)
147+ if (icon != null ) {
148+ builder = builder.withIcon(icon)
149+ }
150+
151+ // 处理文本替换范围与 Snippet 扩展
152+ val textEdit = item.textEdit
153+ val isSnippet = item.insertTextFormat == InsertTextFormat .Snippet
154+
155+ if (isSnippet) {
156+ // 代码片段:将 LSP Snippet 转换为可插入文本,并尽量定位到首个光标位
157+ val snippetText = item.insertText ? : when {
158+ textEdit != null && textEdit.isLeft -> textEdit.left.newText
159+ else -> item.label
160+ }
161+ val parsed = parseLspSnippet(snippetText)
162+ builder = builder.withInsertHandler { context, _ ->
163+ val doc = context.document
164+ val caretModel = context.editor.caretModel
165+
166+ // 先移除默认插入的 lookupString,避免重复文本
167+ if (context.startOffset <= context.tailOffset) {
168+ doc.replaceString(context.startOffset, context.tailOffset, " " )
169+ }
170+
171+ if (textEdit != null && textEdit.isLeft) {
172+ val edit = textEdit.left
173+ val startOffset = toOffset(doc, edit.range.start)
174+ val endOffset = toOffset(doc, edit.range.end)
175+ doc.replaceString(startOffset, endOffset, parsed.text)
176+ val caretOffset = startOffset + parsed.caretOffset.coerceAtLeast(0 ).coerceAtMost(parsed.text.length)
177+ caretModel.moveToOffset(caretOffset)
178+ } else {
179+ val insertionOffset = context.startOffset
180+ doc.insertString(insertionOffset, parsed.text)
181+ val caretOffset = insertionOffset + parsed.caretOffset.coerceAtLeast(0 ).coerceAtMost(parsed.text.length)
182+ caretModel.moveToOffset(caretOffset)
183+ }
184+ }
185+ } else if (textEdit != null && textEdit.isLeft) {
186+ val edit = textEdit.left
187+ builder = builder.withInsertHandler { context, _ ->
188+ val doc = context.document
189+ // 先移除默认插入的 lookupString,避免重复文本
190+ if (context.startOffset <= context.tailOffset) {
191+ doc.replaceString(context.startOffset, context.tailOffset, " " )
192+ }
193+ // 使用 TextEdit 中定义的范围进行替换
194+ val startOffset = toOffset(doc, edit.range.start)
195+ val endOffset = toOffset(doc, edit.range.end)
196+ doc.replaceString(startOffset, endOffset, edit.newText)
197+ context.editor.caretModel.moveToOffset(startOffset + edit.newText.length)
198+ }
125199 }
126200
127201 return builder
128202 }
129203
204+ /* *
205+ * 将 LSP Position 转换为文档偏移量
206+ */
207+ private fun toOffset (document : com.intellij.openapi.editor.Document , position : Position ): Int {
208+ val line = position.line.coerceIn(0 , document.lineCount - 1 )
209+ val lineStartOffset = document.getLineStartOffset(line)
210+ val lineEndOffset = document.getLineEndOffset(line)
211+ val character = position.character
212+
213+ return (lineStartOffset + character).coerceIn(lineStartOffset, lineEndOffset)
214+ }
215+
130216 /* *
131217 * 根据 CompletionItemKind 获取对应的图标
132218 */
133- private fun getIconForKind (kind : org.eclipse.lsp4j.CompletionItemKind ): javax.swing.Icon ? {
219+ private fun getIconForKind (kind : CompletionItemKind ? ): javax.swing.Icon ? {
220+ if (kind == null ) return null
221+
134222 return when (kind) {
135- org.eclipse.lsp4j.CompletionItemKind .Function -> com.intellij.icons.AllIcons .Nodes .Function
136- org.eclipse.lsp4j.CompletionItemKind .Variable -> com.intellij.icons.AllIcons .Nodes .Variable
137- org.eclipse.lsp4j.CompletionItemKind .Class -> com.intellij.icons.AllIcons .Nodes .Class
138- org.eclipse.lsp4j.CompletionItemKind .Interface -> com.intellij.icons.AllIcons .Nodes .Interface
139- org.eclipse.lsp4j.CompletionItemKind .Module -> com.intellij.icons.AllIcons .Nodes .Module
140- org.eclipse.lsp4j.CompletionItemKind .Property -> com.intellij.icons.AllIcons .Nodes .Property
141- org.eclipse.lsp4j.CompletionItemKind .Keyword -> com.intellij.icons.AllIcons .Nodes .Static
223+ CompletionItemKind .Function -> com.intellij.icons.AllIcons .Nodes .Function
224+ CompletionItemKind .Variable -> com.intellij.icons.AllIcons .Nodes .Variable
225+ CompletionItemKind .Class -> com.intellij.icons.AllIcons .Nodes .Class
226+ CompletionItemKind .Interface -> com.intellij.icons.AllIcons .Nodes .Interface
227+ CompletionItemKind .Module -> com.intellij.icons.AllIcons .Nodes .Module
228+ CompletionItemKind .Property -> com.intellij.icons.AllIcons .Nodes .Property
229+ CompletionItemKind .Keyword -> com.intellij.icons.AllIcons .Nodes .Static
230+ CompletionItemKind .Method -> com.intellij.icons.AllIcons .Nodes .Method
231+ CompletionItemKind .Constructor -> com.intellij.icons.AllIcons .Nodes .Class
232+ CompletionItemKind .Field -> com.intellij.icons.AllIcons .Nodes .Field
233+ CompletionItemKind .Enum -> com.intellij.icons.AllIcons .Nodes .Enum
234+ CompletionItemKind .EnumMember -> com.intellij.icons.AllIcons .Nodes .Enum
235+ CompletionItemKind .Constant -> com.intellij.icons.AllIcons .Nodes .Constant
236+ CompletionItemKind .Struct -> com.intellij.icons.AllIcons .Nodes .Class
237+ CompletionItemKind .Event -> com.intellij.icons.AllIcons .Nodes .ErrorIntroduction
238+ CompletionItemKind .Operator -> com.intellij.icons.AllIcons .Nodes .Function
239+ CompletionItemKind .Unit -> com.intellij.icons.AllIcons .Nodes .Static
240+ CompletionItemKind .Value -> com.intellij.icons.AllIcons .Nodes .Variable
241+ CompletionItemKind .File -> com.intellij.icons.AllIcons .FileTypes .Text
242+ CompletionItemKind .Snippet -> com.intellij.icons.AllIcons .Actions .Copy
243+ CompletionItemKind .Color -> com.intellij.icons.AllIcons .Actions .Colors
244+ CompletionItemKind .Reference -> com.intellij.icons.AllIcons .Nodes .UpLevel
142245 else -> null
143246 }
144247 }
248+
249+ /* *
250+ * 解析 LSP Snippet 文本,返回可插入的纯文本与光标相对位置
251+ * 只实现常见占位符:$1、${1}、${1:default}、${1|a,b|}、$0
252+ * 复杂表达式会被降级为其文字值
253+ */
254+ private fun parseLspSnippet (snippet : String ): ParsedSnippetResult {
255+ var textBuilder = StringBuilder ()
256+ var index = 0
257+ var firstTabstopOffset: Int? = null
258+ var caretAbsoluteIndexForFirst: Int? = null
259+
260+ fun recordFirstCaretIfNeeded (currentOutputLen : Int ) {
261+ if (firstTabstopOffset == null ) {
262+ firstTabstopOffset = currentOutputLen
263+ }
264+ }
265+
266+ while (index < snippet.length) {
267+ val ch = snippet[index]
268+ if (ch == ' $' ) {
269+ // 尝试匹配 $0 / $1..$9 / ${...}
270+ if (index + 1 < snippet.length && snippet[index + 1 ].isDigit()) {
271+ val num = snippet[index + 1 ]
272+ if (num == ' 0' ) {
273+ // $0 作为最终光标位置,若未设置首个 tabstop,则使用此处
274+ recordFirstCaretIfNeeded(textBuilder.length)
275+ } else {
276+ // $1..$9 将光标定位到第一个 tabstop 出现位置
277+ recordFirstCaretIfNeeded(textBuilder.length)
278+ }
279+ index + = 2
280+ continue
281+ } else if (index + 1 < snippet.length && snippet[index + 1 ] == ' {' ) {
282+ // 处理 ${...}
283+ val start = index + 2
284+ var i = start
285+ var braceDepth = 1
286+ while (i < snippet.length && braceDepth > 0 ) {
287+ val c = snippet[i]
288+ if (c == ' {' ) braceDepth++
289+ if (c == ' }' ) braceDepth--
290+ i++
291+ }
292+ val content = snippet.substring(start, i - 1 )
293+ // 解析形如 1:default 或 1|a,b|
294+ val colonIdx = content.indexOf(' :' )
295+ val pipeStart = content.indexOf(' |' )
296+ val pipeEnd = if (pipeStart >= 0 ) content.indexOf(' |' , pipeStart + 1 ) else - 1
297+ val defaultText = when {
298+ colonIdx >= 0 -> content.substring(colonIdx + 1 )
299+ pipeStart >= 0 && pipeEnd > pipeStart -> content.substring(pipeStart + 1 , pipeEnd).split(' ,' ).firstOrNull() ? : " "
300+ else -> " "
301+ }
302+ // 变量编号,用于判断是否首个 tabstop
303+ val varNum = content.takeWhile { it.isDigit() }
304+ if (varNum.isNotEmpty() && varNum != " 0" ) {
305+ recordFirstCaretIfNeeded(textBuilder.length)
306+ } else if (varNum == " 0" ) {
307+ recordFirstCaretIfNeeded(textBuilder.length)
308+ }
309+ textBuilder.append(defaultText)
310+ index = i
311+ continue
312+ }
313+ } else if (ch == ' \\ ' && index + 1 < snippet.length) {
314+ // 处理简单转义 \$、\{、\}
315+ val next = snippet[index + 1 ]
316+ if (next == ' $' || next == ' {' || next == ' }' ) {
317+ textBuilder.append(next)
318+ index + = 2
319+ continue
320+ }
321+ }
322+ textBuilder.append(ch)
323+ index++
324+ }
325+
326+ val finalText = textBuilder.toString()
327+ val caretOffset = (firstTabstopOffset ? : finalText.length)
328+ return ParsedSnippetResult (finalText, caretOffset)
329+ }
330+
331+ /* *
332+ * 解析后的 Snippet 结果
333+ */
334+ private data class ParsedSnippetResult (
335+ val text : String ,
336+ val caretOffset : Int
337+ )
145338 }
146339}
0 commit comments