@@ -2,11 +2,15 @@ package com.termux.zerocore.aidebug
22
33import android.content.Context
44import com.google.gson.Gson
5+ import com.termux.shared.termux.TermuxConstants
56import com.termux.zerocore.editor.lsp.EditorClangdSupport
7+ import com.termux.zerocore.editor.lsp.EditorJdtClassFileSupport
68import com.termux.zerocore.editor.lsp.EditorJdtLsSupport
79import com.termux.zerocore.editor.lsp.EditorLspDebugStore
810import com.termux.zerocore.editor.lsp.EditorLspInstaller
911import com.termux.zerocore.editor.lsp.EditorLspManager
12+ import com.termux.zerocore.editor.lsp.EditorLspUris
13+ import java.io.File
1014
1115object ZtAiDebugEditorLspHelper {
1216 private val gson = Gson ()
@@ -30,7 +34,8 @@ object ZtAiDebugEditorLspHelper {
3034 " launcher" to EditorJdtLsSupport .findLauncherJar()?.absolutePath,
3135 " config_template" to EditorJdtLsSupport .findConfigTemplateDir()?.absolutePath,
3236 " config_runtime" to EditorJdtLsSupport .ensureRuntimeConfigDir()?.absolutePath,
33- " arch" to (System .getProperty(" os.arch" ) ? : " unknown" )
37+ " arch" to (System .getProperty(" os.arch" ) ? : " unknown" ),
38+ " navigation_timeout_ms" to EditorJdtLsSupport .NAVIGATION_TIMEOUT_MILLIS
3439 ),
3540 " c_clangd" to mapOf (
3641 " package_id" to EditorClangdSupport .PACKAGE_ID ,
@@ -48,7 +53,7 @@ object ZtAiDebugEditorLspHelper {
4853 " diagnostics" to EditorLspDebugStore .diagnosticsSnapshot(),
4954 " recent_events" to EditorLspDebugStore .recentEvents(50 ),
5055 " stderr_tail" to EditorLspDebugStore .stderrTail(80 ),
51- " hint_zh" to " 打开编辑器并启用 LSP 后,diagnostics 会随 publishDiagnostics 更新;stderr_tail 为 jdt-ls 日志(不再 Toast) 。"
56+ " hint_zh" to " 打开编辑器并启用 LSP 后可用 POST /api/editor/lsp/definition 探测转到定义 。"
5257 )
5358 )
5459 }
@@ -73,4 +78,164 @@ object ZtAiDebugEditorLspHelper {
7378 )
7479 )
7580 }
81+
82+ /* *
83+ * 探测 textDocument/definition(及 typeDefinition 回退)与 class 源码解析。
84+ * JSON: {path?, line?, column?, word?, occurrence?}
85+ */
86+ fun definitionJson (
87+ path : String? ,
88+ line : Int? ,
89+ column : Int? ,
90+ word : String? ,
91+ occurrence : Int = 0,
92+ navigate : Boolean = false
93+ ): String {
94+ val manager = EditorLspManager .activeInstance
95+ ? : return gson.toJson(
96+ mapOf (
97+ " ok" to false ,
98+ " error" to " editor_lsp_inactive" ,
99+ " hint_zh" to " 先 POST /api/editor/open 打开 Java 文件并等待 LSP 就绪(GET /api/editor/lsp/status)"
100+ )
101+ )
102+ val file = resolveProbeFile(path, manager)
103+ ? : return gson.toJson(mapOf (" ok" to false , " error" to " file_not_found" , " path" to path))
104+ val languageId = EditorLspManager .languageIdForExtension(file.extension)
105+ ? : return gson.toJson(
106+ mapOf (
107+ " ok" to false ,
108+ " error" to " unsupported_language" ,
109+ " file" to file.absolutePath
110+ )
111+ )
112+ val content = runCatching { file.readText() }.getOrElse {
113+ return gson.toJson(
114+ mapOf (
115+ " ok" to false ,
116+ " error" to " read_failed" ,
117+ " detail" to (it.message ? : " " )
118+ )
119+ )
120+ }
121+ val pos = resolveProbePosition(content, line, column, word, occurrence)
122+ ? : return gson.toJson(
123+ mapOf (
124+ " ok" to false ,
125+ " error" to " position_not_found" ,
126+ " word" to word,
127+ " line" to line,
128+ " column" to column
129+ )
130+ )
131+ runCatching { manager.openDocument(file, languageId, content) }
132+ val started = System .currentTimeMillis()
133+ val probe = manager.definitionDetailed(file, languageId, pos.first, pos.second)
134+ val locations = probe.locations
135+ val resolved = locations.map { loc ->
136+ val target = manager.resolveNavigationLocation(file, languageId, loc)
137+ mapOf (
138+ " raw_uri" to loc.uri,
139+ " raw_file" to loc.file.absolutePath,
140+ " raw_line" to loc.line,
141+ " raw_column" to loc.column,
142+ " needs_class_contents" to EditorJdtClassFileSupport .needsClassFileContents(loc),
143+ " resolved_file" to target?.file?.absolutePath,
144+ " resolved_exists" to (target?.file?.isFile == true ),
145+ " resolved_size" to (target?.file?.takeIf { it.isFile }?.length() ? : - 1L ),
146+ " resolved_line" to target?.line,
147+ " resolved_column" to target?.column
148+ )
149+ }
150+ val elapsed = System .currentTimeMillis() - started
151+ val snippetLine = content.lineSequence().elementAtOrNull(pos.first).orEmpty()
152+ var navigated = false
153+ var navigateError: String? = null
154+ if (navigate) {
155+ val target = locations.firstOrNull()?.let { loc ->
156+ manager.resolveNavigationLocation(file, languageId, loc)
157+ }
158+ if (target != null && target.file.isFile) {
159+ navigated = manager.openLocationInHost(target)
160+ if (! navigated) navigateError = " host_open_failed"
161+ } else {
162+ navigateError = " no_resolved_target"
163+ }
164+ }
165+ return gson.toJson(
166+ mapOf (
167+ " ok" to true ,
168+ " file" to file.absolutePath,
169+ " language_id" to languageId,
170+ " query" to mapOf (
171+ " line" to pos.first,
172+ " column" to pos.second,
173+ " word" to word,
174+ " line_text" to snippetLine,
175+ " uri" to EditorLspUris .forFile(file)
176+ ),
177+ " count" to locations.size,
178+ " elapsed_ms" to elapsed,
179+ " probe_source" to probe.source,
180+ " prefer_type" to probe.preferType,
181+ " raw_definition" to probe.rawDefinition,
182+ " raw_typeDefinition" to probe.rawTypeDefinition,
183+ " locations" to resolved,
184+ " navigate" to navigate,
185+ " navigated" to navigated,
186+ " navigate_error" to navigateError,
187+ " hint_zh" to if (locations.isEmpty()) {
188+ " 定义为空:看 raw_definition;方法名应走 definition,类型名才走 typeDefinition"
189+ } else {
190+ " resolved_line/column 即为方法在源码中的位置;navigate=true 可在编辑器打开"
191+ },
192+ " recent_events" to EditorLspDebugStore .recentEvents(15 )
193+ )
194+ )
195+ }
196+
197+ private fun resolveProbeFile (path : String? , manager : EditorLspManager ): File ? {
198+ val trimmed = path?.trim().orEmpty()
199+ if (trimmed.isNotEmpty()) {
200+ val direct = File (trimmed)
201+ if (direct.isFile) return direct
202+ val underHome = File (TermuxConstants .TERMUX_HOME_DIR , trimmed.removePrefix(" /" ))
203+ if (underHome.isFile) return underHome
204+ return null
205+ }
206+ val open = manager.debugStatus()[" open_documents" ] as ? List <* >
207+ val firstUri = open?.firstOrNull()?.toString().orEmpty()
208+ if (firstUri.isBlank()) return null
209+ val p = EditorLspUris .pathOf(firstUri)
210+ return p.takeIf { it.isNotEmpty() }?.let { File (it) }?.takeIf { it.isFile }
211+ }
212+
213+ private fun resolveProbePosition (
214+ content : String ,
215+ line : Int? ,
216+ column : Int? ,
217+ word : String? ,
218+ occurrence : Int
219+ ): Pair <Int , Int >? {
220+ val w = word?.trim().orEmpty()
221+ if (w.isNotEmpty()) {
222+ var seen = 0
223+ content.lineSequence().forEachIndexed { idx, text ->
224+ var from = 0
225+ while (true ) {
226+ val at = text.indexOf(w, from)
227+ if (at < 0 ) break
228+ if (seen == occurrence.coerceAtLeast(0 )) {
229+ val mid = at + w.length / 2
230+ return idx to mid
231+ }
232+ seen++
233+ from = at + w.length
234+ }
235+ }
236+ return null
237+ }
238+ if (line == null || column == null ) return null
239+ return line.coerceAtLeast(0 ) to column.coerceAtLeast(0 )
240+ }
76241}
0 commit comments