Skip to content

Commit 3bb7e5f

Browse files
committed
inti
1 parent 5ccfe5e commit 3bb7e5f

9 files changed

Lines changed: 804 additions & 70 deletions

File tree

build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ dependencies {
1515
}
1616

1717
intellij {
18-
version.set("2023.3")
18+
// 使用本地已安装的 IntelliJ IDEA,避免远程下载
19+
localPath.set("/Applications/IntelliJ IDEA.app/Contents")
1920
type.set("IC")
2021
downloadSources.set(false)
22+
plugins.set(listOf("com.intellij.java"))
2123
}
2224

2325
tasks {

src/main/kotlin/com/company/plugin/completion/ZyCompletionContributor.kt

Lines changed: 213 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ package com.company.plugin.completion
22

33
import com.company.plugin.lsp.ZyLspService
44
import com.intellij.codeInsight.completion.*
5+
import com.intellij.codeInsight.lookup.LookupElement
56
import com.intellij.codeInsight.lookup.LookupElementBuilder
67
import com.intellij.openapi.diagnostic.Logger
78
import com.intellij.openapi.vfs.VirtualFile
89
import com.intellij.patterns.PlatformPatterns
910
import com.intellij.psi.PsiElement
1011
import com.intellij.util.ProcessingContext
1112
import org.eclipse.lsp4j.CompletionItem
13+
import org.eclipse.lsp4j.CompletionItemKind
1214
import 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
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.company.plugin.language
2+
3+
import com.intellij.lexer.LexerBase
4+
import com.intellij.openapi.util.TextRange
5+
import com.intellij.psi.tree.IElementType
6+
import com.intellij.psi.TokenType
7+
8+
/**
9+
* ZY 词法分析器(极简版)
10+
* 将文本拆分为 IDENTIFIER / WHITE_SPACE / OTHER 三类,避免整文件成为单一 PSI 元素
11+
*/
12+
class ZyLexer : LexerBase() {
13+
private var buffer: CharSequence = ""
14+
private var startOffset: Int = 0
15+
private var endOffset: Int = 0
16+
private var tokenStart: Int = 0
17+
private var tokenEnd: Int = 0
18+
private var tokenType: IElementType? = null
19+
20+
override fun start(buffer: CharSequence, startOffset: Int, endOffset: Int, initialState: Int) {
21+
this.buffer = buffer
22+
this.startOffset = startOffset
23+
this.endOffset = endOffset
24+
this.tokenStart = startOffset
25+
this.tokenEnd = startOffset
26+
this.tokenType = null
27+
advance()
28+
}
29+
30+
override fun getState(): Int = 0
31+
32+
override fun getTokenType(): IElementType? = tokenType
33+
34+
override fun getTokenStart(): Int = tokenStart
35+
36+
override fun getTokenEnd(): Int = tokenEnd
37+
38+
override fun getBufferSequence(): CharSequence = buffer
39+
40+
override fun getBufferEnd(): Int = endOffset
41+
42+
override fun advance() {
43+
if (tokenEnd >= endOffset) {
44+
tokenType = null
45+
return
46+
}
47+
48+
tokenStart = if (tokenEnd == 0) startOffset else tokenEnd
49+
var i = tokenStart
50+
if (i >= endOffset) {
51+
tokenType = null
52+
return
53+
}
54+
55+
val ch = buffer[i]
56+
if (ch.isWhitespace()) {
57+
while (i < endOffset && buffer[i].isWhitespace()) i++
58+
tokenEnd = i
59+
tokenType = ZyTokenTypes.WHITE_SPACE
60+
return
61+
}
62+
63+
if (isWordChar(ch)) {
64+
while (i < endOffset && isWordChar(buffer[i])) i++
65+
tokenEnd = i
66+
tokenType = ZyTokenTypes.IDENTIFIER
67+
return
68+
}
69+
70+
// 其他字符
71+
tokenEnd = i + 1
72+
tokenType = ZyTokenTypes.OTHER
73+
}
74+
75+
private fun Char.isWhitespace(): Boolean = this == ' ' || this == '\t' || this == '\n' || this == '\r'
76+
private fun isWordChar(c: Char): Boolean = c.isLetterOrDigit() || c == '_'
77+
}
78+
79+

0 commit comments

Comments
 (0)