From 3daf5e1a273f5d612049ccaab6ed9fa7faaab177 Mon Sep 17 00:00:00 2001 From: sirily11 <32106111+sirily11@users.noreply.github.com> Date: Fri, 5 Jun 2026 19:33:44 +0800 Subject: [PATCH 1/4] feat: order briefing cards by project order Sort briefing cards to follow the sidebar project order first, then by most-recently-updated within each project, across desktop, iOS/iPad, and Android. Projects not in the list sort last. Co-Authored-By: Claude Opus 4.8 (1M context) --- RxCode/Views/Sidebar/BriefingView.swift | 18 ++++++++++++- .../rxcode/ui/briefing/BriefingGrouping.kt | 16 ++++++++++++ .../rxcode/ui/briefing/BriefingScreen.kt | 7 ++++-- RxCodeMobile/Views/MobileBriefingView.swift | 25 ++++++++++++++++++- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/RxCode/Views/Sidebar/BriefingView.swift b/RxCode/Views/Sidebar/BriefingView.swift index 0ebf2e11..dcd28626 100644 --- a/RxCode/Views/Sidebar/BriefingView.swift +++ b/RxCode/Views/Sidebar/BriefingView.swift @@ -66,6 +66,12 @@ struct BriefingView: View { _ = appState.branchBriefingRevision _ = appState.threadSummaryRevision + // Position of each project in the sidebar order, so briefing cards can be + // arranged to follow the project list rather than pure recency. + let projectOrder: [UUID: Int] = Dictionary( + uniqueKeysWithValues: appState.projects.enumerated().map { ($0.element.id, $0.offset) } + ) + let knownIds = knownProjectIds let briefings = appState.threadStore.allBranchBriefingItems() .filter { knownIds.contains($0.projectId) } @@ -105,7 +111,17 @@ struct BriefingView: View { updatedAt: $0.updated ) } - .sorted { $0.updatedAt > $1.updatedAt } + .sorted { lhs, rhs in + // Primary: follow the sidebar project order. Unknown projects + // (no position) sort after known ones. Within the same project, + // fall back to most-recently-updated first. + let lhsOrder = projectOrder[lhs.projectId] ?? Int.max + let rhsOrder = projectOrder[rhs.projectId] ?? Int.max + if lhsOrder != rhsOrder { + return lhsOrder < rhsOrder + } + return lhs.updatedAt > rhs.updatedAt + } let projectFiltered: [BriefingGroup] if selectedProjectIds.isEmpty { diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingGrouping.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingGrouping.kt index 7372564f..eb60ab69 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingGrouping.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingGrouping.kt @@ -68,3 +68,19 @@ fun groupBriefings( return buckets.values.sortedByDescending { it.updatedAt } } + +/** + * Reorder briefing groups to follow the sidebar project order (the order of + * [projectIds]). Groups whose project isn't listed sort last; within the same + * project the most recently updated comes first. Mirrors the desktop + * `BriefingView` sort and Swift `sortedByProjectOrder`. + */ +fun List.sortedByProjectOrder( + projectIds: List, +): List { + val order = projectIds.withIndex().associate { (index, id) -> id to index } + return sortedWith( + compareBy { order[it.projectId] ?: Int.MAX_VALUE } + .thenByDescending { it.updatedAt } + ) +} diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingScreen.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingScreen.kt index 5246be93..fc4669a8 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingScreen.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingScreen.kt @@ -100,8 +100,11 @@ fun BriefingScreen( .eachCount() } } - val allGroups by remember(state.branchBriefings, state.threadSummaries) { - derivedStateOf { groupBriefings(state.branchBriefings, state.threadSummaries) } + val allGroups by remember(state.branchBriefings, state.threadSummaries, state.projects) { + derivedStateOf { + groupBriefings(state.branchBriefings, state.threadSummaries) + .sortedByProjectOrder(state.projects.map { it.id }) + } } val visibleGroups by remember(allGroups, selectedProjectIds, showAllBranches, state.projectBranches) { derivedStateOf { diff --git a/RxCodeMobile/Views/MobileBriefingView.swift b/RxCodeMobile/Views/MobileBriefingView.swift index a1109259..cf3a087d 100644 --- a/RxCodeMobile/Views/MobileBriefingView.swift +++ b/RxCodeMobile/Views/MobileBriefingView.swift @@ -147,9 +147,12 @@ struct MobileBriefingView: View { return counts } - /// Every briefing group before the project/branch filters are applied. + /// Every briefing group before the project/branch filters are applied, + /// ordered to follow the sidebar project order (then most-recent first + /// within a project) so cards line up with the project list. private var allGroups: [GroupedBriefing] { groupBriefings(briefings: state.branchBriefings, threads: state.threadSummaries) + .sortedByProjectOrder(state.projects) } /// Briefing groups after applying the active project and branch filters. @@ -355,6 +358,7 @@ struct BriefingListView: View { private var allGroups: [GroupedBriefing] { groupBriefings(briefings: state.branchBriefings, threads: state.threadSummaries) + .sortedByProjectOrder(state.projects) } private var groups: [GroupedBriefing] { @@ -822,3 +826,22 @@ func groupBriefings( return buckets.values.sorted { $0.updatedAt > $1.updatedAt } } + +extension Array where Element == GroupedBriefing { + /// Reorder briefing groups to follow the given project order. Groups whose + /// project isn't in the list sort last; within the same project the most + /// recently updated comes first. Mirrors the desktop `BriefingView` sort. + func sortedByProjectOrder(_ projects: [Project]) -> [GroupedBriefing] { + let order: [UUID: Int] = Dictionary( + uniqueKeysWithValues: projects.enumerated().map { ($0.element.id, $0.offset) } + ) + return sorted { lhs, rhs in + let lhsOrder = order[lhs.projectId] ?? Int.max + let rhsOrder = order[rhs.projectId] ?? Int.max + if lhsOrder != rhsOrder { + return lhsOrder < rhsOrder + } + return lhs.updatedAt > rhs.updatedAt + } + } +} From c1ffa6b3a073275896b08906b61e06c8525d0deb Mon Sep 17 00:00:00 2001 From: sirily11 <32106111+sirily11@users.noreply.github.com> Date: Fri, 5 Jun 2026 19:37:50 +0800 Subject: [PATCH 2/4] feat: skip lifecycle hooks for automation threads and cap PR title length - Pass skipHooks for menu-, commit-, and CI-fix-spawned threads so utility runs don't trigger code review / auto-continue lifecycle hooks - Truncate generated PR titles to at most 20 words and update the summarization prompt accordingly - Simplify SFSymbolPicker to a search-only grid, dropping free-text entry - Add tests for PR title truncation Co-Authored-By: Claude Opus 4.8 (1M context) --- RxCode/App/AppState+CIStatus.swift | 6 +- RxCode/App/AppState+Commit.swift | 1 + RxCode/App/AppState+Hooks.swift | 6 +- RxCode/App/AppState+MenuDispatch.swift | 7 ++- RxCode/App/AppState+PullRequest.swift | 17 ++++++ RxCode/App/AppState.swift | 7 +++ RxCode/Resources/Localizable.xcstrings | 57 ++++++++++++------- .../Services/OpenAISummarizationService.swift | 2 +- .../Settings/CustomMenusSettingsTab.swift | 3 + RxCode/Views/Settings/SFSymbolPicker.swift | 9 +-- RxCodeTests/AppStateTests.swift | 47 +++++++++++++++ 11 files changed, 129 insertions(+), 33 deletions(-) diff --git a/RxCode/App/AppState+CIStatus.swift b/RxCode/App/AppState+CIStatus.swift index 7f91277f..0847be51 100644 --- a/RxCode/App/AppState+CIStatus.swift +++ b/RxCode/App/AppState+CIStatus.swift @@ -181,7 +181,8 @@ extension AppState { projectId: project.id, threadId: nil, prompt: prompt, - waitForResponse: false + waitForResponse: false, + skipHooks: true ) logger.info("Started auto CI-fix thread for project \(project.name, privacy: .public)") } catch { @@ -208,7 +209,8 @@ extension AppState { projectId: project.id, threadId: nil, prompt: prompt, - waitForResponse: false + waitForResponse: false, + skipHooks: true ) if let error = result.error { throw CodeReviewError.sendFailed(error) } return result.threadId diff --git a/RxCode/App/AppState+Commit.swift b/RxCode/App/AppState+Commit.swift index 45501c08..3d56c976 100644 --- a/RxCode/App/AppState+Commit.swift +++ b/RxCode/App/AppState+Commit.swift @@ -109,6 +109,7 @@ extension AppState { waitForResponse: false, timeoutSeconds: 600, threadLabel: Self.manualCommitLabel, + skipHooks: true, setupKind: HookSetupKind.commitPush ) if let error = result.error { throw CommitFilesError.sendFailed(error) } diff --git a/RxCode/App/AppState+Hooks.swift b/RxCode/App/AppState+Hooks.swift index 12418454..5891f47c 100644 --- a/RxCode/App/AppState+Hooks.swift +++ b/RxCode/App/AppState+Hooks.swift @@ -48,10 +48,14 @@ extension AppState { /// the desktop's own locale (used by native menus). Mobile relay requests pass /// the phone's locale so titles come back translated. func projectContextMenuItems(for project: Project, branch: String? = nil, locale: String? = nil) -> [MenuItem] { - hookManager.projectContextMenuItems(ProjectContextMenuPayload(project: project, branch: branch, locale: locale)) + // Touch the revision so SwiftUI re-runs this fetch (and thus picks up + // newly created/edited custom items) when called from a view body. + _ = customMenuItemsRevision + return hookManager.projectContextMenuItems(ProjectContextMenuPayload(project: project, branch: branch, locale: locale)) } func threadContextMenuItems(for session: ChatSession.Summary, locale: String? = nil) -> [MenuItem] { + _ = customMenuItemsRevision guard let project = projects.first(where: { $0.id == session.projectId }) else { return [] } return hookManager.threadContextMenuItems(ThreadContextMenuPayload(project: project, session: session, locale: locale)) } diff --git a/RxCode/App/AppState+MenuDispatch.swift b/RxCode/App/AppState+MenuDispatch.swift index 23d280b3..a367e2bc 100644 --- a/RxCode/App/AppState+MenuDispatch.swift +++ b/RxCode/App/AppState+MenuDispatch.swift @@ -108,11 +108,16 @@ extension AppState { case .createThread: let project = try requireProject(command.projectId) + // A menu-spawned thread is a utility/automation run (commit, review, + // a user's custom action) — it shouldn't itself trigger lifecycle + // hooks like code review or auto-continue. Mirrors the built-in + // commit / code-review / CI-fix spawners. let result = try await sendCrossProject( projectId: project.id, threadId: nil, prompt: config.message ?? "", - waitForResponse: false + waitForResponse: false, + skipHooks: true ) if let error = result.error { throw MenuDispatchError.customActionFailed(error) } return MenuCommandResult(threadId: result.threadId) diff --git a/RxCode/App/AppState+PullRequest.swift b/RxCode/App/AppState+PullRequest.swift index c40f3301..cb39b70d 100644 --- a/RxCode/App/AppState+PullRequest.swift +++ b/RxCode/App/AppState+PullRequest.swift @@ -162,6 +162,22 @@ extension AppState { "test", "build", "ci", "chore", "revert" ] + /// Maximum number of whitespace-separated words allowed in a generated PR + /// title (including the Conventional-Commit `:` prefix). Keeps titles + /// short and scannable even when the model ignores the prompt's length hint. + static let maxPullRequestTitleWords = 20 + + /// Truncate `title` to at most ``maxPullRequestTitleWords`` whitespace- + /// separated words. Returns the title unchanged when already within the + /// limit; otherwise keeps the leading words and strips any trailing + /// punctuation left dangling by the cut. + static func truncatePullRequestTitleWords(_ title: String) -> String { + let words = title.split(whereSeparator: { $0.isWhitespace }) + guard words.count > maxPullRequestTitleWords else { return title } + let kept = words.prefix(maxPullRequestTitleWords).joined(separator: " ") + return stripTrailingPullRequestTitlePunctuation(kept) + } + /// True when `title` matches `(): ` and /// `` is one of ``conventionalCommitTypes``. Used to gate generated PR /// titles so a non-conforming title triggers a model retry. @@ -217,6 +233,7 @@ extension AppState { .trimmingCharacters(in: CharacterSet(charactersIn: "\"'`")) .trimmingCharacters(in: .whitespaces) title = normalizePullRequestTitle(title, fallbackTitle: fallbackTitle) + title = truncatePullRequestTitleWords(title) if title.isEmpty { title = fallbackTitle } let body = lines[(firstIdx + 1)...] diff --git a/RxCode/App/AppState.swift b/RxCode/App/AppState.swift index d041b03a..5fbf355e 100644 --- a/RxCode/App/AppState.swift +++ b/RxCode/App/AppState.swift @@ -239,6 +239,13 @@ final class AppState { /// changes without an accompanying observable property mutation. var todoSnapshotsRevision: Int = 0 + /// Bumped each time a custom context-menu item is created, edited, toggled, + /// or removed (`CustomMenuItemRecord` in SwiftData). The sidebar's project / + /// thread context menus read this when building `hookMenuItems` so SwiftUI + /// re-runs the `customMenuItems(...)` fetch and the new item appears without + /// an app restart. + var customMenuItemsRevision: Int = 0 + /// Pending permission/question prompts keyed by hook id. This mirrors the /// per-window queues so mobile thread rows can show the same attention state. var mobilePendingRequests: [String: PermissionRequest] = [:] diff --git a/RxCode/Resources/Localizable.xcstrings b/RxCode/Resources/Localizable.xcstrings index 279d3a13..59c335a6 100644 --- a/RxCode/Resources/Localizable.xcstrings +++ b/RxCode/Resources/Localizable.xcstrings @@ -1627,7 +1627,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "向项目、线程和简报卡片菜单中添加你自己的项。它们会自动同步到移动设备。" + "value" : "向项目、聊天和简报卡片菜单中添加你自己的项。它们会自动同步到移动设备。" } } } @@ -1698,7 +1698,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "会话完成后,更改会发送到一个不运行任何 Hook 的关联 [Code Review] 线程。如果审查要求修改,其备注会发送回此线程,以便智能体持续修复并重新接受审查(最多 3 次)。" + "value" : "会话完成后,更改会发送到一个不运行任何 Hook 的关联 [Code Review] 聊天。如果审查要求修改,其备注会发送回此聊天,以便智能体持续修复并重新接受审查(最多 3 次)。" } } } @@ -1714,7 +1714,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "会话完成后,你的消息会发送给助手——作为此线程中的后续消息,或发送到一个不运行任何 Hook 的新关联线程。设置条件后,模型会先判断是/否。每个会话触发一次,发送新消息时重置。" + "value" : "会话完成后,你的消息会发送给助手——作为此聊天中的后续消息,或发送到一个不运行任何 Hook 的新关联聊天。设置条件后,模型会先判断是/否。每个会话触发一次,发送新消息时重置。" } } } @@ -2006,6 +2006,7 @@ } }, "An SF Symbol name shown beside the title, e.g. \"bolt\"." : { + "extractionState" : "stale", "localizations" : { "ko" : { "stringUnit" : { @@ -2020,6 +2021,9 @@ } } } + }, + "An SF Symbol shown beside the title, e.g. \"bolt\"." : { + }, "API Key" : { "localizations" : { @@ -2962,7 +2966,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "在“设置 → 上下文菜单”中构建你自己的项目、线程和简报卡片菜单项。它们会自动同步到你的手机。" + "value" : "在“设置 → 上下文菜单”中构建你自己的项目、聊天和简报卡片菜单项。它们会自动同步到你的手机。" } } } @@ -3581,7 +3585,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "审查此线程" + "value" : "审查此聊天" } } } @@ -4174,6 +4178,7 @@ } }, "Context Menus" : { + "extractionState" : "stale", "localizations" : { "ko" : { "stringUnit" : { @@ -4216,7 +4221,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "继续现有线程" + "value" : "继续现有聊天" } } } @@ -4232,7 +4237,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "继续线程" + "value" : "继续聊天" } } } @@ -4427,7 +4432,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "使用模板消息创建新线程或继续现有线程。" + "value" : "使用模板消息创建新聊天或继续现有聊天。" } } } @@ -4508,7 +4513,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "创建新线程" + "value" : "创建新聊天" } } } @@ -6554,7 +6559,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "未通过的审查会发送回原始线程,以便智能体修复并重新接受审查。" + "value" : "未通过的审查会发送回原始聊天,以便智能体修复并重新接受审查。" } } } @@ -7514,6 +7519,9 @@ } } } + }, + "Icon" : { + }, "Images" : { "localizations" : { @@ -9393,7 +9401,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "新建线程" + "value" : "新建聊天" } } } @@ -9409,7 +9417,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "新线程标签" + "value" : "新聊天标签" } } } @@ -9834,6 +9842,9 @@ } } } + }, + "No matches." : { + }, "No matching memories" : { "localizations" : { @@ -11363,7 +11374,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "选择执行审查的模型——默认与线程使用相同的模型。" + "value" : "选择执行审查的模型——默认与聊天使用相同的模型。" } } } @@ -12991,7 +13002,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "在会话结束后运行,并在关联线程中审查已修改的文件。" + "value" : "在会话结束后运行,并在关联聊天中审查已修改的文件。" } } } @@ -13204,7 +13215,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "与线程相同" + "value" : "与聊天相同" } } } @@ -13220,7 +13231,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "同一线程" + "value" : "同一聊天" } } } @@ -13665,6 +13676,9 @@ } } } + }, + "Search symbols" : { + }, "Search Threads (⌘K)" : { "extractionState" : "stale", @@ -14350,6 +14364,7 @@ } }, "SF Symbol" : { + "extractionState" : "stale", "localizations" : { "ko" : { "stringUnit" : { @@ -15358,7 +15373,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "目标线程 ID" + "value" : "目标聊天 ID" } } } @@ -15502,7 +15517,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "要继续的线程的会话 ID。保留 {{sessionId}} 占位符可指向所点按的线程。" + "value" : "要继续的聊天的会话 ID。保留 {{sessionId}} 占位符可指向所点按的聊天。" } } } @@ -15754,7 +15769,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "线程菜单" + "value" : "聊天菜单" } } } @@ -15770,7 +15785,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "线程模型" + "value" : "聊天模型" } } } @@ -16885,4 +16900,4 @@ } }, "version" : "1.1" -} +} \ No newline at end of file diff --git a/RxCode/Services/OpenAISummarizationService.swift b/RxCode/Services/OpenAISummarizationService.swift index f7d53e9f..c4866368 100644 --- a/RxCode/Services/OpenAISummarizationService.swift +++ b/RxCode/Services/OpenAISummarizationService.swift @@ -271,7 +271,7 @@ actor OpenAISummarizationService { Write a GitHub pull request title and description that summarize the work on a branch, using the branch briefing below. Format rules (MUST follow exactly): - - The FIRST line is the PR title in Conventional Commits format: `(): ` — under 72 characters, lowercase imperative mood, no trailing period. + - The FIRST line is the PR title in Conventional Commits format: `(): ` — at most 20 words and under 72 characters, lowercase imperative mood, no trailing period. - `` MUST be EXACTLY one of these tokens, spelled verbatim: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert. - Use the short token only. Do NOT expand or substitute it (e.g. write `feat`, never `feature`; `perf`, never `performance`). A title whose type is not on the list above is invalid. - Then exactly ONE blank line. diff --git a/RxCode/Views/Settings/CustomMenusSettingsTab.swift b/RxCode/Views/Settings/CustomMenusSettingsTab.swift index 9042698c..b927bed8 100644 --- a/RxCode/Views/Settings/CustomMenusSettingsTab.swift +++ b/RxCode/Views/Settings/CustomMenusSettingsTab.swift @@ -24,6 +24,7 @@ struct CustomMenusSettingsSection: View { .sheet(item: $editing) { draft in CustomMenuEditorSheet(draft: draft, projects: appState.projects) { result in appState.threadStore.upsertCustomMenuItem(result.toRecord()) + appState.customMenuItemsRevision += 1 reload() } } @@ -33,6 +34,7 @@ struct CustomMenusSettingsSection: View { let id = record.id pendingRemoval = nil appState.threadStore.deleteCustomMenuItem(id: id) + appState.customMenuItemsRevision += 1 reload() } } message: { record in @@ -105,6 +107,7 @@ struct CustomMenusSettingsSection: View { onToggle: { enabled in record.isEnabled = enabled appState.threadStore.upsertCustomMenuItem(record) + appState.customMenuItemsRevision += 1 reload() }, onRemove: { pendingRemoval = record } diff --git a/RxCode/Views/Settings/SFSymbolPicker.swift b/RxCode/Views/Settings/SFSymbolPicker.swift index fa3a77b8..cd8da6eb 100644 --- a/RxCode/Views/Settings/SFSymbolPicker.swift +++ b/RxCode/Views/Settings/SFSymbolPicker.swift @@ -2,8 +2,7 @@ import RxCodeCore import SwiftUI /// A dropdown control for choosing an SF Symbol. Shows a live preview of the -/// current selection and opens a searchable grid of common symbols. Users can -/// also type any symbol name directly for symbols not in the curated list. +/// current selection and opens a searchable grid of common symbols. struct SFSymbolPicker: View { @Binding var symbol: String @@ -75,10 +74,6 @@ struct SFSymbolPicker: View { private var pickerBody: some View { VStack(spacing: 10) { - TextField("Symbol name", text: $symbol, prompt: Text(verbatim: "e.g. bolt")) - .textFieldStyle(.roundedBorder) - .font(.system(size: ClaudeTheme.size(12), design: .monospaced)) - HStack(spacing: 6) { Image(systemName: "magnifyingglass") .font(.system(size: ClaudeTheme.size(11))) @@ -103,7 +98,7 @@ struct SFSymbolPicker: View { .frame(height: 220) if filtered.isEmpty { - Text("No matches. Type a name above to use it anyway.") + Text("No matches.") .font(.system(size: ClaudeTheme.size(10))) .foregroundStyle(.secondary) } diff --git a/RxCodeTests/AppStateTests.swift b/RxCodeTests/AppStateTests.swift index 67847c5f..2dfe3caa 100644 --- a/RxCodeTests/AppStateTests.swift +++ b/RxCodeTests/AppStateTests.swift @@ -564,6 +564,53 @@ final class AppStateTests: XCTestCase { XCTAssertFalse(AppState.isConventionalCommitTitle("")) } + func testParsePullRequestContentTruncatesTitleToTwentyWords() { + let raw = """ + feat: add one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty + + Body text. + """ + + let result = AppState.parsePullRequestContent(raw, branch: "context-menu") + + let wordCount = result.title.split(whereSeparator: { $0.isWhitespace }).count + XCTAssertEqual(wordCount, AppState.maxPullRequestTitleWords) + XCTAssertEqual( + result.title, + "feat: add one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen" + ) + XCTAssertEqual(result.body, "Body text.") + } + + func testParsePullRequestContentKeepsShortTitleUnchanged() { + let raw = """ + fix: correct the crash on launch + + Body text. + """ + + let result = AppState.parsePullRequestContent(raw, branch: "context-menu") + + XCTAssertEqual(result.title, "fix: correct the crash on launch") + } + + func testTruncatePullRequestTitleWordsStripsDanglingPunctuation() { + // 21 words where the 20th word ends in a period; truncation must drop the + // trailing punctuation left dangling at the cut. + let title = "feat: add one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen. nineteen" + + let truncated = AppState.truncatePullRequestTitleWords(title) + + XCTAssertEqual( + truncated, + "feat: add one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen" + ) + XCTAssertEqual( + truncated.split(whereSeparator: { $0.isWhitespace }).count, + AppState.maxPullRequestTitleWords + ) + } + // MARK: - Helpers private func makeProject(_ name: String) -> Project { From 9d0d93ef0f6a2f42e18e21ab01473e9a6e80a089 Mon Sep 17 00:00:00 2001 From: sirily11 <32106111+sirily11@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:26:27 +0800 Subject: [PATCH 3/4] fix: swift build --- Packages/Package.resolved | 227 ++++++++- Packages/Package.swift | 86 +++- .../Models/CustomMenuItemRecord.swift | 31 +- .../Utilities/SyntaxHighlighter.swift | 11 + .../RxCodeEditor/CodeEditorAutocomplete.swift | 156 +++++++ .../Sources/RxCodeEditor/CodeEditorView.swift | 178 +++++++ .../Sources/RxCodeMarkdown/MarkdownView.swift | 17 +- .../TreeSitterScanners/csrc/css_scanner.c | 100 ++++ .../csrc/javascript_scanner.c | 364 +++++++++++++++ .../TreeSitterScanners/csrc/lua_scanner.c | 195 ++++++++ .../TreeSitterScanners/csrc/python_scanner.c | 437 ++++++++++++++++++ .../csrc/tree_sitter/alloc.h | 54 +++ .../csrc/tree_sitter/array.h | 291 ++++++++++++ .../csrc/tree_sitter/parser.h | 286 ++++++++++++ .../include/treesitter_scanners.h | 2 + .../PredefinedAutocompleteProviderTests.swift | 72 +++ RxCode.xcodeproj/project.pbxproj | 24 +- .../xcshareddata/swiftpm/Package.resolved | 225 +++++++++ RxCode/Resources/Localizable.xcstrings | 70 ++- .../Services/Hooks/hooks/CustomMenuHook.swift | 7 +- RxCode/Services/ThreadStore+CustomMenus.swift | 9 +- .../Settings/CustomMenuEditorSheet.swift | 93 +++- .../Settings/CustomMenusSettingsTab.swift | 6 +- RxCode/Views/Settings/JSONCodeEditor.swift | 30 ++ RxCode/Views/Sidebar/FileInspectorView.swift | 10 +- RxCodeAndroid/app/build.gradle.kts | 13 + .../src/main/assets/queries/c/highlights.scm | 81 ++++ .../main/assets/queries/cpp/highlights.scm | 70 +++ .../main/assets/queries/java/highlights.scm | 149 ++++++ .../main/assets/queries/json/highlights.scm | 16 + .../main/assets/queries/kotlin/highlights.scm | 380 +++++++++++++++ .../main/assets/queries/python/highlights.scm | 137 ++++++ .../main/assets/queries/xml/highlights.scm | 168 +++++++ .../app/rxlab/rxcode/RxCodeApplication.kt | 4 + .../ui/briefing/BriefingDetailScreen.kt | 3 +- .../app/rxlab/rxcode/ui/chat/ChatScreen.kt | 3 +- .../app/rxlab/rxcode/ui/util/CodeBlock.kt | 201 ++++++++ .../rxlab/rxcode/ui/util/CodeHighlighter.kt | 233 ++++++++++ RxCodeAndroid/gradle/libs.versions.toml | 15 + 39 files changed, 4396 insertions(+), 58 deletions(-) create mode 100644 Packages/Sources/RxCodeEditor/CodeEditorAutocomplete.swift create mode 100644 Packages/Sources/RxCodeEditor/CodeEditorView.swift create mode 100644 Packages/Sources/TreeSitterScanners/csrc/css_scanner.c create mode 100644 Packages/Sources/TreeSitterScanners/csrc/javascript_scanner.c create mode 100644 Packages/Sources/TreeSitterScanners/csrc/lua_scanner.c create mode 100644 Packages/Sources/TreeSitterScanners/csrc/python_scanner.c create mode 100644 Packages/Sources/TreeSitterScanners/csrc/tree_sitter/alloc.h create mode 100644 Packages/Sources/TreeSitterScanners/csrc/tree_sitter/array.h create mode 100644 Packages/Sources/TreeSitterScanners/csrc/tree_sitter/parser.h create mode 100644 Packages/Sources/TreeSitterScanners/include/treesitter_scanners.h create mode 100644 Packages/Tests/RxCodeEditorTests/PredefinedAutocompleteProviderTests.swift create mode 100644 RxCode/Views/Settings/JSONCodeEditor.swift create mode 100644 RxCodeAndroid/app/src/main/assets/queries/c/highlights.scm create mode 100644 RxCodeAndroid/app/src/main/assets/queries/cpp/highlights.scm create mode 100644 RxCodeAndroid/app/src/main/assets/queries/java/highlights.scm create mode 100644 RxCodeAndroid/app/src/main/assets/queries/json/highlights.scm create mode 100644 RxCodeAndroid/app/src/main/assets/queries/kotlin/highlights.scm create mode 100644 RxCodeAndroid/app/src/main/assets/queries/python/highlights.scm create mode 100644 RxCodeAndroid/app/src/main/assets/queries/xml/highlights.scm create mode 100644 RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/util/CodeBlock.kt create mode 100644 RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/util/CodeHighlighter.kt diff --git a/Packages/Package.resolved b/Packages/Package.resolved index 557968cf..2f8dab8f 100644 --- a/Packages/Package.resolved +++ b/Packages/Package.resolved @@ -1,6 +1,231 @@ { - "originHash" : "3127591c474948afb96f0c81df8be151fe447da11fef447bbb933b0712661346", + "originHash" : "b2291e8317a0a55453eb864270a1cacdf570f9cfaed9a3389b87e7e509694a46", "pins" : [ + { + "identity" : "swift-tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/swift-tree-sitter", + "state" : { + "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", + "version" : "0.25.0" + } + }, + { + "identity" : "tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter", + "state" : { + "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406", + "version" : "0.25.10" + } + }, + { + "identity" : "tree-sitter-bash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-bash", + "state" : { + "revision" : "a06c2e4415e9bc0346c6b86d401879ffb44058f7", + "version" : "0.25.1" + } + }, + { + "identity" : "tree-sitter-c", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-c", + "state" : { + "revision" : "b780e47fc780ddc8da13afa35a3f4ed5c157823d", + "version" : "0.24.2" + } + }, + { + "identity" : "tree-sitter-c-sharp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-c-sharp", + "state" : { + "revision" : "cac6d5fb595f5811a076336682d5d595ac1c9e85", + "version" : "0.23.5" + } + }, + { + "identity" : "tree-sitter-cpp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-cpp", + "state" : { + "revision" : "f41e1a044c8a84ea9fa8577fdd2eab92ec96de02", + "version" : "0.23.4" + } + }, + { + "identity" : "tree-sitter-css", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-css", + "state" : { + "revision" : "dda5cfc5722c429eaba1c910ca32c2c0c5bb1a3f", + "version" : "0.25.0" + } + }, + { + "identity" : "tree-sitter-go", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-go", + "state" : { + "revision" : "1547678a9da59885853f5f5cc8a99cc203fa2e2c", + "version" : "0.25.0" + } + }, + { + "identity" : "tree-sitter-haskell", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-haskell", + "state" : { + "revision" : "c30d812bc90827f1a54106a25bc9a6307f5cdcec", + "version" : "0.23.1" + } + }, + { + "identity" : "tree-sitter-html", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-html", + "state" : { + "revision" : "5a5ca8551a179998360b4a4ca2c0f366a35acc03", + "version" : "0.23.2" + } + }, + { + "identity" : "tree-sitter-java", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-java", + "state" : { + "revision" : "94703d5a6bed02b98e438d7cad1136c01a60ba2c", + "version" : "0.23.5" + } + }, + { + "identity" : "tree-sitter-javascript", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-javascript", + "state" : { + "revision" : "44c892e0be055ac465d5eeddae6d3e194424e7de", + "version" : "0.25.0" + } + }, + { + "identity" : "tree-sitter-json", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-json", + "state" : { + "revision" : "ee35a6ebefcef0c5c416c0d1ccec7370cfca5a24", + "version" : "0.24.8" + } + }, + { + "identity" : "tree-sitter-kotlin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/fwcd/tree-sitter-kotlin", + "state" : { + "revision" : "e1a2d5ad1f61f5740677183cd4125bb071cd2f30", + "version" : "0.3.8" + } + }, + { + "identity" : "tree-sitter-lua", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter-grammars/tree-sitter-lua", + "state" : { + "revision" : "10fe0054734eec83049514ea2e718b2a56acd0c9", + "version" : "0.5.0" + } + }, + { + "identity" : "tree-sitter-markdown", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter-grammars/tree-sitter-markdown", + "state" : { + "revision" : "f969cd3ae3f9fbd4e43205431d0ae286014c05b5", + "version" : "0.5.3" + } + }, + { + "identity" : "tree-sitter-php", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-php", + "state" : { + "revision" : "5b5627faaa290d89eb3d01b9bf47c3bb9e797dea", + "version" : "0.24.2" + } + }, + { + "identity" : "tree-sitter-python", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-python", + "state" : { + "revision" : "293fdc02038ee2bf0e2e206711b69c90ac0d413f", + "version" : "0.25.0" + } + }, + { + "identity" : "tree-sitter-ruby", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-ruby", + "state" : { + "revision" : "71bd32fb7607035768799732addba884a37a6210", + "version" : "0.23.1" + } + }, + { + "identity" : "tree-sitter-rust", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-rust", + "state" : { + "revision" : "77a3747266f4d621d0757825e6b11edcbf991ca5", + "version" : "0.24.2" + } + }, + { + "identity" : "tree-sitter-scala", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-scala", + "state" : { + "revision" : "38950b525c9dfc44c8b60d44bdd6e54217286ca8", + "version" : "0.26.0" + } + }, + { + "identity" : "tree-sitter-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/alex-pinkus/tree-sitter-swift", + "state" : { + "revision" : "31d17fe7e818a2048c808b5c6fdc2dc792f4f5b5", + "version" : "0.7.3-with-generated-files" + } + }, + { + "identity" : "tree-sitter-toml", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter-grammars/tree-sitter-toml", + "state" : { + "revision" : "64b56832c2cffe41758f28e05c756a3a98d16f41", + "version" : "0.7.0" + } + }, + { + "identity" : "tree-sitter-typescript", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-typescript", + "state" : { + "revision" : "f975a621f4e7f532fe322e13c4f79495e0a7b2e7", + "version" : "0.23.2" + } + }, + { + "identity" : "tree-sitter-yaml", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter-grammars/tree-sitter-yaml", + "state" : { + "revision" : "b733d3f5f5005890f324333dd57e1f0badec5c87", + "version" : "0.7.0" + } + }, { "identity" : "viewinspector", "kind" : "remoteSourceControl", diff --git a/Packages/Package.swift b/Packages/Package.swift index 8c8633b7..9ea38a92 100644 --- a/Packages/Package.swift +++ b/Packages/Package.swift @@ -1,6 +1,66 @@ // swift-tools-version: 6.2 import PackageDescription +// Tree-sitter grammar packages used by `SyntaxHighlighter`. Each one ships a +// `tree_sitter_()` parser plus a `queries/highlights.scm` bundle that +// `LanguageConfiguration(_:name:)` auto-discovers. Their own SwiftTreeSitter +// dependency is test-only, so SwiftPM prunes it and the mix of ChimeHQ- and +// tree-sitter-org-hosted SwiftTreeSitter URLs does not conflict with ours. +let grammarPackages: [Package.Dependency] = [ + .package(url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.25.0"), + .package(url: "https://github.com/alex-pinkus/tree-sitter-swift", exact: "0.7.3-with-generated-files"), + .package(url: "https://github.com/tree-sitter/tree-sitter-javascript", exact: "0.25.0"), + .package(url: "https://github.com/tree-sitter/tree-sitter-typescript", exact: "0.23.2"), + .package(url: "https://github.com/tree-sitter/tree-sitter-python", exact: "0.25.0"), + .package(url: "https://github.com/tree-sitter/tree-sitter-json", exact: "0.24.8"), + .package(url: "https://github.com/tree-sitter/tree-sitter-bash", exact: "0.25.1"), + .package(url: "https://github.com/tree-sitter/tree-sitter-go", exact: "0.25.0"), + .package(url: "https://github.com/tree-sitter/tree-sitter-rust", exact: "0.24.2"), + .package(url: "https://github.com/tree-sitter/tree-sitter-ruby", exact: "0.23.1"), + .package(url: "https://github.com/tree-sitter/tree-sitter-java", exact: "0.23.5"), + .package(url: "https://github.com/tree-sitter/tree-sitter-c", exact: "0.24.2"), + .package(url: "https://github.com/tree-sitter/tree-sitter-cpp", exact: "0.23.4"), + .package(url: "https://github.com/tree-sitter/tree-sitter-c-sharp", exact: "0.23.5"), + .package(url: "https://github.com/tree-sitter/tree-sitter-html", exact: "0.23.2"), + .package(url: "https://github.com/tree-sitter/tree-sitter-css", exact: "0.25.0"), + .package(url: "https://github.com/tree-sitter/tree-sitter-php", exact: "0.24.2"), + .package(url: "https://github.com/tree-sitter/tree-sitter-scala", exact: "0.26.0"), + .package(url: "https://github.com/tree-sitter/tree-sitter-haskell", exact: "0.23.1"), + .package(url: "https://github.com/tree-sitter-grammars/tree-sitter-yaml", exact: "0.7.0"), + .package(url: "https://github.com/tree-sitter-grammars/tree-sitter-toml", exact: "0.7.0"), + .package(url: "https://github.com/tree-sitter-grammars/tree-sitter-lua", exact: "0.5.0"), + .package(url: "https://github.com/tree-sitter-grammars/tree-sitter-markdown", exact: "0.5.3"), + .package(url: "https://github.com/fwcd/tree-sitter-kotlin", exact: "0.3.8"), +] + +// Grammar products linked into RxCodeCore (where `SyntaxHighlighter` lives). +let grammarProducts: [Target.Dependency] = [ + .product(name: "SwiftTreeSitter", package: "swift-tree-sitter"), + .product(name: "TreeSitterSwift", package: "tree-sitter-swift"), + .product(name: "TreeSitterJavaScript", package: "tree-sitter-javascript"), + .product(name: "TreeSitterTypeScript", package: "tree-sitter-typescript"), + .product(name: "TreeSitterPython", package: "tree-sitter-python"), + .product(name: "TreeSitterJSON", package: "tree-sitter-json"), + .product(name: "TreeSitterBash", package: "tree-sitter-bash"), + .product(name: "TreeSitterGo", package: "tree-sitter-go"), + .product(name: "TreeSitterRust", package: "tree-sitter-rust"), + .product(name: "TreeSitterRuby", package: "tree-sitter-ruby"), + .product(name: "TreeSitterJava", package: "tree-sitter-java"), + .product(name: "TreeSitterC", package: "tree-sitter-c"), + .product(name: "TreeSitterCPP", package: "tree-sitter-cpp"), + .product(name: "TreeSitterCSharp", package: "tree-sitter-c-sharp"), + .product(name: "TreeSitterHTML", package: "tree-sitter-html"), + .product(name: "TreeSitterCSS", package: "tree-sitter-css"), + .product(name: "TreeSitterPHP", package: "tree-sitter-php"), + .product(name: "TreeSitterScala", package: "tree-sitter-scala"), + .product(name: "TreeSitterHaskell", package: "tree-sitter-haskell"), + .product(name: "TreeSitterYAML", package: "tree-sitter-yaml"), + .product(name: "TreeSitterTOML", package: "tree-sitter-toml"), + .product(name: "TreeSitterLua", package: "tree-sitter-lua"), + .product(name: "TreeSitterMarkdown", package: "tree-sitter-markdown"), + .product(name: "TreeSitterKotlin", package: "tree-sitter-kotlin"), +] + let package = Package( name: "RxCodePackages", defaultLocalization: "en", @@ -9,13 +69,14 @@ let package = Package( .library(name: "MessageList", targets: ["MessageList"]), .library(name: "RxCodeCore", targets: ["RxCodeCore"]), .library(name: "RxCodeChatKit", targets: ["RxCodeChatKit"]), + .library(name: "RxCodeEditor", targets: ["RxCodeEditor"]), .library(name: "RxCodeMarkdown", targets: ["RxCodeMarkdown"]), .library(name: "RxCodeSync", targets: ["RxCodeSync"]), .library(name: "DiffView", targets: ["DiffView"]), ], dependencies: [ .package(url: "https://github.com/nalexn/ViewInspector", from: "0.10.0"), - ], + ] + grammarPackages, targets: [ .target( name: "MessageList", @@ -26,10 +87,23 @@ let package = Package( ), .target( name: "RxCodeCore", + dependencies: grammarProducts + ["TreeSitterScanners"], path: "Sources/RxCodeCore" ), + // Provides external-scanner symbols (`tree_sitter__external_scanner_*`) + // for grammar packages whose SPM manifest omits `scanner.c` when consumed + // as a dependency (a CWD-relative `fileExists` check). Vendored from the + // matching grammar tags. + .target( + name: "TreeSitterScanners", + path: "Sources/TreeSitterScanners", + sources: ["csrc"], + publicHeadersPath: "include", + cSettings: [.headerSearchPath("csrc")] + ), .target( name: "RxCodeMarkdown", + dependencies: ["RxCodeCore"], path: "Sources/RxCodeMarkdown" ), .target( @@ -53,6 +127,11 @@ let package = Package( dependencies: ["RxCodeCore"], path: "Sources/RxCodeSync" ), + .target( + name: "RxCodeEditor", + dependencies: ["RxCodeCore"], + path: "Sources/RxCodeEditor" + ), .target( name: "DiffView", dependencies: ["RxCodeCore"], @@ -88,6 +167,11 @@ let package = Package( ], path: "Tests/RxCodeChatKitTests" ), + .testTarget( + name: "RxCodeEditorTests", + dependencies: ["RxCodeEditor"], + path: "Tests/RxCodeEditorTests" + ), .testTarget( name: "RxCodeMarkdownTests", dependencies: ["RxCodeMarkdown"], diff --git a/Packages/Sources/RxCodeCore/Models/CustomMenuItemRecord.swift b/Packages/Sources/RxCodeCore/Models/CustomMenuItemRecord.swift index ab9f6002..66c441e9 100644 --- a/Packages/Sources/RxCodeCore/Models/CustomMenuItemRecord.swift +++ b/Packages/Sources/RxCodeCore/Models/CustomMenuItemRecord.swift @@ -7,8 +7,9 @@ import SwiftData /// surface; mobile fetches the same items over the relay and renders them /// identically, so no separate sync channel is needed. /// -/// `surface` selects where the item appears (project / thread / briefing-card menu), -/// `projectId == nil` means "all projects", and `actionKind` + the action fields +/// `surface` selects where the item appears (any combination of project / thread / +/// briefing-card menus, stored comma-separated), `projectId == nil` means +/// "all projects", and `actionKind` + the action fields /// describe the work the desktop performs when the item is tapped. Template fields /// (`urlString`, `bodyTemplate`, `messageTemplate`, header values) may contain /// context placeholders such as `{{projectName}}`, `{{projectPath}}`, @@ -36,7 +37,8 @@ public final class CustomMenuItemRecord { public var systemImage: String? /// `nil` => available in every project; otherwise scoped to this project. public var projectId: UUID? - /// Raw `Surface` value. + /// Comma-separated raw `Surface` values (e.g. `"project,thread"`). A single + /// legacy value (`"project"`) parses identically, so old records keep working. public var surface: String /// Raw `ActionKind` value. public var actionKind: String @@ -63,7 +65,7 @@ public final class CustomMenuItemRecord { title: String, systemImage: String? = nil, projectId: UUID? = nil, - surface: Surface, + surfaces: [Surface], actionKind: ActionKind, httpMethod: String? = nil, urlString: String? = nil, @@ -80,7 +82,7 @@ public final class CustomMenuItemRecord { self.title = title self.systemImage = systemImage self.projectId = projectId - self.surface = surface.rawValue + self.surface = CustomMenuItemRecord.encodeSurfaces(surfaces) self.actionKind = actionKind.rawValue self.httpMethod = httpMethod self.urlString = urlString @@ -94,9 +96,26 @@ public final class CustomMenuItemRecord { self.updatedAt = updatedAt } - public var surfaceValue: Surface { Surface(rawValue: surface) ?? .project } + /// All surfaces this item attaches to, in the stored order. Falls back to + /// `[.project]` for an empty/unparseable value so the item never disappears. + public var surfaces: [Surface] { + let parsed = surface + .split(separator: ",") + .compactMap { Surface(rawValue: $0.trimmingCharacters(in: .whitespaces)) } + return parsed.isEmpty ? [.project] : parsed + } + + /// First surface — kept for callers that only need a single representative value. + public var surfaceValue: Surface { surfaces.first ?? .project } public var actionKindValue: ActionKind { ActionKind(rawValue: actionKind) ?? .createThread } + /// Encode a surface list to the stored comma-separated representation. Falls + /// back to `project` when empty so a record always has a home. + public static func encodeSurfaces(_ surfaces: [Surface]) -> String { + let list = surfaces.isEmpty ? [Surface.project] : surfaces + return list.map(\.rawValue).joined(separator: ",") + } + /// Decoded header map (empty when unset or malformed). public var headers: [String: String] { guard let headersJSON, let data = headersJSON.data(using: .utf8) else { return [:] } diff --git a/Packages/Sources/RxCodeCore/Utilities/SyntaxHighlighter.swift b/Packages/Sources/RxCodeCore/Utilities/SyntaxHighlighter.swift index 29eba89d..0287ebdd 100644 --- a/Packages/Sources/RxCodeCore/Utilities/SyntaxHighlighter.swift +++ b/Packages/Sources/RxCodeCore/Utilities/SyntaxHighlighter.swift @@ -8,6 +8,17 @@ import UIKit // MARK: - Syntax Highlighter public enum SyntaxHighlighter { + /// Async version of `highlightNS` that runs highlighting on a background thread. + public static func highlightNSAsync(_ code: String, language: String, fontSize: CGFloat = 12) async -> NSAttributedString { + struct SendableWrapper: @unchecked Sendable { + let value: NSAttributedString + } + let wrapped = await Task.detached { + SendableWrapper(value: highlightNS(code, language: language, fontSize: fontSize)) + }.value + return wrapped.value + } + public static func highlightNS(_ code: String, language: String, fontSize: CGFloat = 12) -> NSAttributedString { let normalized = normalizeLanguage(language) let tokens = tokenize(code, language: normalized) diff --git a/Packages/Sources/RxCodeEditor/CodeEditorAutocomplete.swift b/Packages/Sources/RxCodeEditor/CodeEditorAutocomplete.swift new file mode 100644 index 00000000..d82a03c2 --- /dev/null +++ b/Packages/Sources/RxCodeEditor/CodeEditorAutocomplete.swift @@ -0,0 +1,156 @@ +import Foundation + +public struct CodeEditorAutocompleteItem: Hashable, Identifiable, Sendable { + public let id: String + public let title: String + public let insertionText: String + public let detail: String? + + public init( + id: String? = nil, + title: String, + insertionText: String, + detail: String? = nil + ) { + self.id = id ?? insertionText + self.title = title + self.insertionText = insertionText + self.detail = detail + } +} + +public struct CodeEditorAutocompleteContext: Hashable, Sendable { + public let text: String + public let selectedRange: NSRange + public let replacementRange: NSRange + public let query: String + + public init( + text: String, + selectedRange: NSRange, + replacementRange: NSRange, + query: String + ) { + self.text = text + self.selectedRange = selectedRange + self.replacementRange = replacementRange + self.query = query + } +} + +public protocol CodeEditorAutocompleteProvider: Sendable { + /// Return a completion context when the caret is in a provider-owned trigger + /// region. LSP-backed providers can use this boundary to map the buffer and + /// caret into a protocol request before returning cached or fetched items. + func context(in text: String, selectedRange: NSRange) -> CodeEditorAutocompleteContext? + func completions(for context: CodeEditorAutocompleteContext) -> [CodeEditorAutocompleteItem] +} + +public struct PredefinedAutocompleteProvider: CodeEditorAutocompleteProvider { + public let trigger: String + public let closingDelimiter: String? + public let items: [CodeEditorAutocompleteItem] + + public init( + trigger: String, + closingDelimiter: String? = nil, + items: [CodeEditorAutocompleteItem] + ) { + self.trigger = trigger + self.closingDelimiter = closingDelimiter + self.items = items + } + + public func context(in text: String, selectedRange: NSRange) -> CodeEditorAutocompleteContext? { + guard selectedRange.length == 0, + !trigger.isEmpty, + selectedRange.location <= (text as NSString).length + else { return nil } + + let nsText = text as NSString + let prefixRange = NSRange(location: 0, length: selectedRange.location) + let triggerRange = nsText.range(of: trigger, options: [.backwards], range: prefixRange) + guard triggerRange.location != NSNotFound else { return nil } + + let queryStart = triggerRange.location + triggerRange.length + let queryLength = selectedRange.location - queryStart + guard queryLength >= 0 else { return nil } + + let queryRange = NSRange(location: queryStart, length: queryLength) + let query = nsText.substring(with: queryRange) + guard isValidQuery(query) else { return nil } + + if let closingDelimiter, + !closingDelimiter.isEmpty, + query.contains(closingDelimiter) { + return nil + } + + return CodeEditorAutocompleteContext( + text: text, + selectedRange: selectedRange, + replacementRange: NSRange( + location: triggerRange.location, + length: selectedRange.location - triggerRange.location + ), + query: query + ) + } + + public func completions(for context: CodeEditorAutocompleteContext) -> [CodeEditorAutocompleteItem] { + let query = context.query.trimmingCharacters(in: .whitespacesAndNewlines) + guard !query.isEmpty else { return items } + + return items.filter { item in + item.title.localizedCaseInsensitiveContains(query) + || item.insertionText.localizedCaseInsensitiveContains(query) + || (item.detail?.localizedCaseInsensitiveContains(query) ?? false) + } + } + + private func isValidQuery(_ query: String) -> Bool { + for scalar in query.unicodeScalars { + if CharacterSet.whitespacesAndNewlines.contains(scalar) { return false } + switch scalar { + case "{", "}", "\"", "'", "`", ":", ",", "[", "]", "(", ")": + return false + default: + continue + } + } + return true + } +} + +public extension PredefinedAutocompleteProvider { + static func keywords( + _ keywords: [String], + trigger: String, + closingDelimiter: String? = nil + ) -> PredefinedAutocompleteProvider { + PredefinedAutocompleteProvider( + trigger: trigger, + closingDelimiter: closingDelimiter, + items: keywords.map { keyword in + CodeEditorAutocompleteItem( + title: keyword, + insertionText: keyword + ) + } + ) + } + + static func placeholders(_ placeholders: [String]) -> PredefinedAutocompleteProvider { + PredefinedAutocompleteProvider( + trigger: "{{", + closingDelimiter: "}}", + items: placeholders.map { placeholder in + CodeEditorAutocompleteItem( + title: placeholder, + insertionText: "{{\(placeholder)}}", + detail: "Context placeholder" + ) + } + ) + } +} diff --git a/Packages/Sources/RxCodeEditor/CodeEditorView.swift b/Packages/Sources/RxCodeEditor/CodeEditorView.swift new file mode 100644 index 00000000..adc4dd46 --- /dev/null +++ b/Packages/Sources/RxCodeEditor/CodeEditorView.swift @@ -0,0 +1,178 @@ +#if os(macOS) +import AppKit +import RxCodeCore +import SwiftUI + +public struct CodeEditorView: NSViewRepresentable { + @Binding private var text: String + private let language: String + private let fontSize: CGFloat + private let autocompleteProvider: (any CodeEditorAutocompleteProvider)? + + public init( + text: Binding, + language: String, + fontSize: CGFloat = 12, + autocompleteProvider: (any CodeEditorAutocompleteProvider)? = nil + ) { + _text = text + self.language = language + self.fontSize = fontSize + self.autocompleteProvider = autocompleteProvider + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + public func makeNSView(context: Context) -> NSScrollView { + let scrollView = NSScrollView() + scrollView.borderType = .noBorder + scrollView.drawsBackground = false + scrollView.hasVerticalScroller = true + scrollView.hasHorizontalScroller = false + scrollView.autohidesScrollers = true + + let textStorage = NSTextStorage() + let layoutManager = NSLayoutManager() + textStorage.addLayoutManager(layoutManager) + + let containerSize = NSSize(width: scrollView.contentSize.width, height: .greatestFiniteMagnitude) + let textContainer = NSTextContainer(containerSize: containerSize) + textContainer.widthTracksTextView = true + textContainer.heightTracksTextView = false + layoutManager.addTextContainer(textContainer) + + let textView = CompletionTextView(frame: .zero, textContainer: textContainer) + textView.delegate = context.coordinator + textView.completionRangeProvider = context.coordinator + textView.isRichText = false + textView.allowsUndo = true + textView.isVerticallyResizable = true + textView.isHorizontallyResizable = false + textView.autoresizingMask = [.width] + textView.minSize = NSSize(width: 0, height: 0) + textView.maxSize = NSSize( + width: CGFloat.greatestFiniteMagnitude, + height: CGFloat.greatestFiniteMagnitude + ) + textView.textContainerInset = NSSize(width: 6, height: 8) + textView.font = .monospacedSystemFont(ofSize: fontSize, weight: .regular) + textView.backgroundColor = NSColor.textBackgroundColor + textView.isAutomaticQuoteSubstitutionEnabled = false + textView.isAutomaticDashSubstitutionEnabled = false + textView.isAutomaticTextReplacementEnabled = false + textView.isAutomaticSpellingCorrectionEnabled = false + textView.isAutomaticDataDetectionEnabled = false + textView.smartInsertDeleteEnabled = false + + scrollView.documentView = textView + context.coordinator.textView = textView + context.coordinator.apply(text: text) + return scrollView + } + + public func updateNSView(_ scrollView: NSScrollView, context: Context) { + context.coordinator.parent = self + guard let textView = context.coordinator.textView else { return } + if textView.string != text { + context.coordinator.apply(text: text) + } + } + + @MainActor public final class Coordinator: NSObject, NSTextViewDelegate, CompletionRangeProviding { + var parent: CodeEditorView + weak var textView: CompletionTextView? + private var activeCompletionContext: CodeEditorAutocompleteContext? + + init(_ parent: CodeEditorView) { + self.parent = parent + } + + public func textDidChange(_ notification: Notification) { + guard let textView else { return } + parent.text = textView.string + if !textView.hasMarkedText() { + highlight(textView) + triggerAutocompleteIfNeeded(textView) + } + } + + func apply(text: String) { + guard let textView else { return } + if textView.string != text { + textView.string = text + } + highlight(textView) + } + + func completionRange(for textView: NSTextView) -> NSRange? { + activeCompletionContext?.replacementRange + } + + public func textView( + _ textView: NSTextView, + completions words: [String], + forPartialWordRange charRange: NSRange, + indexOfSelectedItem index: UnsafeMutablePointer? + ) -> [String] { + guard let provider = parent.autocompleteProvider, + let context = activeCompletionContext + else { return [] } + + index?.pointee = 0 + return provider.completions(for: context).map(\.insertionText) + } + + private func triggerAutocompleteIfNeeded(_ textView: CompletionTextView) { + guard let provider = parent.autocompleteProvider else { + activeCompletionContext = nil + textView.completionRangeOverride = nil + return + } + + let selectedRange = textView.selectedRange() + guard let context = provider.context(in: textView.string, selectedRange: selectedRange), + !provider.completions(for: context).isEmpty + else { + activeCompletionContext = nil + textView.completionRangeOverride = nil + return + } + + activeCompletionContext = context + textView.completionRangeOverride = context.replacementRange + textView.complete(nil) + } + + private func highlight(_ textView: NSTextView) { + guard let storage = textView.textStorage else { return } + let selected = textView.selectedRanges + let highlighted = SyntaxHighlighter.highlightNS( + textView.string, + language: parent.language, + fontSize: parent.fontSize + ) + storage.beginEditing() + storage.setAttributedString(highlighted) + storage.endEditing() + textView.selectedRanges = selected + } + } +} + +@MainActor protocol CompletionRangeProviding: AnyObject { + func completionRange(for textView: NSTextView) -> NSRange? +} + +@MainActor final class CompletionTextView: NSTextView { + weak var completionRangeProvider: CompletionRangeProviding? + var completionRangeOverride: NSRange? + + override var rangeForUserCompletion: NSRange { + completionRangeOverride + ?? completionRangeProvider?.completionRange(for: self) + ?? super.rangeForUserCompletion + } +} +#endif diff --git a/Packages/Sources/RxCodeMarkdown/MarkdownView.swift b/Packages/Sources/RxCodeMarkdown/MarkdownView.swift index b771fbf6..babe2a8f 100644 --- a/Packages/Sources/RxCodeMarkdown/MarkdownView.swift +++ b/Packages/Sources/RxCodeMarkdown/MarkdownView.swift @@ -1,4 +1,5 @@ import SwiftUI +import RxCodeCore #if canImport(AppKit) import AppKit #elseif canImport(UIKit) @@ -587,9 +588,7 @@ private struct MarkdownCodeBlockView: View { .frame(height: 0.5) ScrollView(.horizontal, showsIndicators: false) { - Text(content) - .font(.system(size: style.bodyFontSize * 0.88, design: .monospaced)) - .foregroundStyle(style.codeTextColor) + codeText .lineSpacing(style.lineSpacing) .fixedSize() .padding(12) @@ -603,6 +602,18 @@ private struct MarkdownCodeBlockView: View { .strokeBorder(style.borderColor, lineWidth: 0.5) ) } + + /// Syntax-highlighted code when a language is specified, + /// otherwise plain monospaced text in the theme's code color. + private var codeText: Text { + let fontSize = style.bodyFontSize * 0.88 + if let language, !language.isEmpty { + return Text(SyntaxHighlighter.highlight(content, language: language, fontSize: fontSize)) + } + return Text(content) + .font(.system(size: fontSize, design: .monospaced)) + .foregroundColor(style.codeTextColor) + } } private struct MarkdownTableView: View { diff --git a/Packages/Sources/TreeSitterScanners/csrc/css_scanner.c b/Packages/Sources/TreeSitterScanners/csrc/css_scanner.c new file mode 100644 index 00000000..ba7dc652 --- /dev/null +++ b/Packages/Sources/TreeSitterScanners/csrc/css_scanner.c @@ -0,0 +1,100 @@ +#include "tree_sitter/parser.h" + +#include + +enum TokenType { + DESCENDANT_OP, + PSEUDO_CLASS_SELECTOR_COLON, + ERROR_RECOVERY, +}; + +static inline void advance(TSLexer *lexer) { lexer->advance(lexer, false); } + +static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } + +void *tree_sitter_css_external_scanner_create() { return NULL; } + +void tree_sitter_css_external_scanner_destroy(void *payload) {} + +unsigned tree_sitter_css_external_scanner_serialize(void *payload, char *buffer) { return 0; } + +void tree_sitter_css_external_scanner_deserialize(void *payload, const char *buffer, unsigned length) {} + +bool tree_sitter_css_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { + if (valid_symbols[ERROR_RECOVERY]) { + return false; + } + + if (iswspace(lexer->lookahead) && valid_symbols[DESCENDANT_OP]) { + lexer->result_symbol = DESCENDANT_OP; + + skip(lexer); + while (iswspace(lexer->lookahead)) { + skip(lexer); + } + lexer->mark_end(lexer); + + if (lexer->lookahead == '#' || lexer->lookahead == '.' || lexer->lookahead == '[' || lexer->lookahead == '-' || + lexer->lookahead == '*' || iswalnum(lexer->lookahead)) { + return true; + } + + if (lexer->lookahead == ':') { + advance(lexer); + if (iswspace(lexer->lookahead)) { + return false; + } + for (;;) { + if (lexer->lookahead == ';' || lexer->lookahead == '}' || lexer->eof(lexer)) { + return false; + } + if (lexer->lookahead == '{') { + return true; + } + advance(lexer); + } + } + } + + if (valid_symbols[PSEUDO_CLASS_SELECTOR_COLON]) { + while (iswspace(lexer->lookahead)) { + skip(lexer); + } + if (lexer->lookahead == ':') { + advance(lexer); + if (lexer->lookahead == ':') { + return false; + } + lexer->mark_end(lexer); + lexer->result_symbol = PSEUDO_CLASS_SELECTOR_COLON; + + // We need a `{` to be a pseudo class selector, a `;` indicates a property. + // This does not apply if we're in a comment, however. + bool in_comment = false; + while (lexer->lookahead != ';' && lexer->lookahead != '}' && !lexer->eof(lexer)) { + advance(lexer); + if (lexer->lookahead == '{' && !in_comment) { + return true; + } + if (lexer->lookahead == '/' && !in_comment) { + advance(lexer); + if (lexer->lookahead == '*') { + in_comment = true; + } + } else if (lexer->lookahead == '*' && in_comment) { + advance(lexer); + if (lexer->lookahead == '/') { + in_comment = false; + } + } + } + + // If we're at eof, and we happened to *not* find an opening brace to indicate we have a pseudo class + // selector, we should *still* return one at EOF. This will improve error recovery, and the malformed code + // can be parsed as an erroneous pseudo-class selector, rather than an erroneous property. + return lexer->eof(lexer); + } + } + + return false; +} diff --git a/Packages/Sources/TreeSitterScanners/csrc/javascript_scanner.c b/Packages/Sources/TreeSitterScanners/csrc/javascript_scanner.c new file mode 100644 index 00000000..795916dd --- /dev/null +++ b/Packages/Sources/TreeSitterScanners/csrc/javascript_scanner.c @@ -0,0 +1,364 @@ +#include "tree_sitter/parser.h" + +#include +#include + +enum TokenType { + AUTOMATIC_SEMICOLON, + TEMPLATE_CHARS, + TERNARY_QMARK, + HTML_COMMENT, + LOGICAL_OR, + ESCAPE_SEQUENCE, + REGEX_PATTERN, + JSX_TEXT, +}; + +void *tree_sitter_javascript_external_scanner_create() { return NULL; } + +void tree_sitter_javascript_external_scanner_destroy(void *p) {} + +unsigned tree_sitter_javascript_external_scanner_serialize(void *payload, char *buffer) { return 0; } + +void tree_sitter_javascript_external_scanner_deserialize(void *p, const char *b, unsigned n) {} + +static inline void advance(TSLexer *lexer) { lexer->advance(lexer, false); } + +static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } + +static bool scan_template_chars(TSLexer *lexer) { + lexer->result_symbol = TEMPLATE_CHARS; + for (bool has_content = false;; has_content = true) { + lexer->mark_end(lexer); + switch (lexer->lookahead) { + case '`': + return has_content; + case '\0': + return false; + case '$': + advance(lexer); + if (lexer->lookahead == '{') { + return has_content; + } + break; + case '\\': + return has_content; + default: + advance(lexer); + } + } +} + +typedef enum { + REJECT, // Semicolon is illegal, ie a syntax error occurred + NO_NEWLINE, // Unclear if semicolon will be legal, continue + ACCEPT, // Semicolon is legal, assuming a comment was encountered +} WhitespaceResult; + +/** + * @param consume If false, only consume enough to check if comment indicates semicolon-legality + */ +static WhitespaceResult scan_whitespace_and_comments(TSLexer *lexer, bool *scanned_comment, bool consume) { + bool saw_block_newline = false; + + for (;;) { + while (iswspace(lexer->lookahead)) { + skip(lexer); + } + + if (lexer->lookahead == '/') { + skip(lexer); + + if (lexer->lookahead == '/') { + skip(lexer); + while (lexer->lookahead != 0 && lexer->lookahead != '\n' && lexer->lookahead != 0x2028 && + lexer->lookahead != 0x2029) { + skip(lexer); + } + *scanned_comment = true; + } else if (lexer->lookahead == '*') { + skip(lexer); + while (lexer->lookahead != 0) { + if (lexer->lookahead == '*') { + skip(lexer); + if (lexer->lookahead == '/') { + skip(lexer); + *scanned_comment = true; + + if (lexer->lookahead != '/' && !consume) { + return saw_block_newline ? ACCEPT : NO_NEWLINE; + } + + break; + } + } else if (lexer->lookahead == '\n' || lexer->lookahead == 0x2028 || lexer->lookahead == 0x2029) { + saw_block_newline = true; + skip(lexer); + } else { + skip(lexer); + } + } + } else { + return REJECT; + } + } else { + return ACCEPT; + } + } +} + +static bool scan_automatic_semicolon(TSLexer *lexer, bool comment_condition, bool *scanned_comment) { + lexer->result_symbol = AUTOMATIC_SEMICOLON; + lexer->mark_end(lexer); + + for (;;) { + if (lexer->lookahead == 0) { + return true; + } + + if (lexer->lookahead == '/') { + WhitespaceResult result = scan_whitespace_and_comments(lexer, scanned_comment, false); + if (result == REJECT) { + return false; + } + + if (result == ACCEPT && comment_condition && lexer->lookahead != ',' && lexer->lookahead != '=') { + return true; + } + } + + if (lexer->lookahead == '}') { + return true; + } + + if (lexer->is_at_included_range_start(lexer)) { + return true; + } + + if (lexer->lookahead == '\n' || lexer->lookahead == 0x2028 || lexer->lookahead == 0x2029) { + break; + } + + if (!iswspace(lexer->lookahead)) { + return false; + } + + skip(lexer); + } + + skip(lexer); + + if (scan_whitespace_and_comments(lexer, scanned_comment, true) == REJECT) { + return false; + } + + switch (lexer->lookahead) { + case '`': + case ',': + case ':': + case ';': + case '*': + case '%': + case '>': + case '<': + case '=': + case '[': + case '(': + case '?': + case '^': + case '|': + case '&': + case '/': + return false; + + // Insert a semicolon before decimals literals but not otherwise. + case '.': + skip(lexer); + return iswdigit(lexer->lookahead); + + // Insert a semicolon before `--` and `++`, but not before binary `+` or `-`. + case '+': + skip(lexer); + return lexer->lookahead == '+'; + case '-': + skip(lexer); + return lexer->lookahead == '-'; + + // Don't insert a semicolon before `!=`, but do insert one before a unary `!`. + case '!': + skip(lexer); + return lexer->lookahead != '='; + + // Don't insert a semicolon before `in` or `instanceof`, but do insert one + // before an identifier. + case 'i': + skip(lexer); + + if (lexer->lookahead != 'n') { + return true; + } + skip(lexer); + + if (!iswalpha(lexer->lookahead)) { + return false; + } + + for (unsigned i = 0; i < 8; i++) { + if (lexer->lookahead != "stanceof"[i]) { + return true; + } + skip(lexer); + } + + if (!iswalpha(lexer->lookahead)) { + return false; + } + break; + + default: + break; + } + + return true; +} + +static bool scan_ternary_qmark(TSLexer *lexer) { + for (;;) { + if (!iswspace(lexer->lookahead)) { + break; + } + skip(lexer); + } + + if (lexer->lookahead == '?') { + advance(lexer); + + if (lexer->lookahead == '?') { + return false; + } + + lexer->mark_end(lexer); + lexer->result_symbol = TERNARY_QMARK; + + if (lexer->lookahead == '.') { + advance(lexer); + if (iswdigit(lexer->lookahead)) { + return true; + } + return false; + } + return true; + } + return false; +} + +static bool scan_html_comment(TSLexer *lexer) { + while (iswspace(lexer->lookahead) || lexer->lookahead == 0x2028 || lexer->lookahead == 0x2029) { + skip(lexer); + } + + const char *comment_start = ""; + + if (lexer->lookahead == '<') { + for (unsigned i = 0; i < 4; i++) { + if (lexer->lookahead != comment_start[i]) { + return false; + } + advance(lexer); + } + } else if (lexer->lookahead == '-') { + for (unsigned i = 0; i < 3; i++) { + if (lexer->lookahead != comment_end[i]) { + return false; + } + advance(lexer); + } + } else { + return false; + } + + while (lexer->lookahead != 0 && lexer->lookahead != '\n' && lexer->lookahead != 0x2028 && + lexer->lookahead != 0x2029) { + advance(lexer); + } + + lexer->result_symbol = HTML_COMMENT; + lexer->mark_end(lexer); + + return true; +} + +static bool scan_jsx_text(TSLexer *lexer) { + // saw_text will be true if we see any non-whitespace content, or any whitespace content that is not a newline and + // does not immediately follow a newline. + bool saw_text = false; + // at_newline will be true if we are currently at a newline, or if we are at whitespace that is not a newline but + // immediately follows a newline. + bool at_newline = false; + + while (lexer->lookahead != 0 && lexer->lookahead != '<' && lexer->lookahead != '>' && lexer->lookahead != '{' && + lexer->lookahead != '}' && lexer->lookahead != '&') { + bool is_wspace = iswspace(lexer->lookahead); + if (lexer->lookahead == '\n') { + at_newline = true; + } else { + // If at_newline is already true, and we see some whitespace, then it must stay true. + // Otherwise, it should be false. + // + // See the table below to determine the logic for computing `saw_text`. + // + // |------------------------------------| + // | at_newline | is_wspace | saw_text | + // |------------|-----------|-----------| + // | false (0) | false (0) | true (1) | + // | false (0) | true (1) | true (1) | + // | true (1) | false (0) | true (1) | + // | true (1) | true (1) | false (0) | + // |------------------------------------| + + at_newline &= is_wspace; + if (!at_newline) { + saw_text = true; + } + } + + advance(lexer); + } + + lexer->result_symbol = JSX_TEXT; + return saw_text; +} + +bool tree_sitter_javascript_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { + if (valid_symbols[TEMPLATE_CHARS]) { + if (valid_symbols[AUTOMATIC_SEMICOLON]) { + return false; + } + return scan_template_chars(lexer); + } + + if (valid_symbols[JSX_TEXT] && scan_jsx_text(lexer)) { + return true; + } + + if (valid_symbols[AUTOMATIC_SEMICOLON]) { + bool scanned_comment = false; + bool ret = scan_automatic_semicolon(lexer, !valid_symbols[LOGICAL_OR], &scanned_comment); + if (!ret && !scanned_comment && valid_symbols[TERNARY_QMARK] && lexer->lookahead == '?') { + return scan_ternary_qmark(lexer); + } + return ret; + } + + if (valid_symbols[TERNARY_QMARK]) { + return scan_ternary_qmark(lexer); + } + + if (valid_symbols[HTML_COMMENT] && !valid_symbols[LOGICAL_OR] && !valid_symbols[ESCAPE_SEQUENCE] && + !valid_symbols[REGEX_PATTERN]) { + return scan_html_comment(lexer); + } + + return false; +} diff --git a/Packages/Sources/TreeSitterScanners/csrc/lua_scanner.c b/Packages/Sources/TreeSitterScanners/csrc/lua_scanner.c new file mode 100644 index 00000000..e257c2dc --- /dev/null +++ b/Packages/Sources/TreeSitterScanners/csrc/lua_scanner.c @@ -0,0 +1,195 @@ +#include +#include "tree_sitter/alloc.h" +#include "tree_sitter/parser.h" +#include + +enum TokenType { + BLOCK_COMMENT_START, + BLOCK_COMMENT_CONTENT, + BLOCK_COMMENT_END, + + BLOCK_STRING_START, + BLOCK_STRING_CONTENT, + BLOCK_STRING_END, +}; + +static inline void consume(TSLexer *lexer) { lexer->advance(lexer, false); } + +static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } + +static inline bool consume_char(char c, TSLexer *lexer) { + if (lexer->lookahead != c) { + return false; + } + + consume(lexer); + return true; +} + +static inline uint8_t consume_and_count_char(char c, TSLexer *lexer) { + uint8_t count = 0; + while (lexer->lookahead == c) { + ++count; + consume(lexer); + } + return count; +} + +static inline void skip_whitespaces(TSLexer *lexer) { + while (iswspace(lexer->lookahead)) { + skip(lexer); + } +} + +typedef struct { + char ending_char; + uint8_t level_count; +} Scanner; + +static inline void reset_state(Scanner *scanner) { + scanner->ending_char = 0; + scanner->level_count = 0; +} + +void *tree_sitter_lua_external_scanner_create() { + Scanner *scanner = ts_calloc(1, sizeof(Scanner)); + return scanner; +} + +void tree_sitter_lua_external_scanner_destroy(void *payload) { + Scanner *scanner = (Scanner *)payload; + ts_free(scanner); +} + +unsigned tree_sitter_lua_external_scanner_serialize(void *payload, char *buffer) { + Scanner *scanner = (Scanner *)payload; + buffer[0] = scanner->ending_char; + buffer[1] = (char)scanner->level_count; + return 2; +} + +void tree_sitter_lua_external_scanner_deserialize(void *payload, const char *buffer, unsigned length) { + Scanner *scanner = (Scanner *)payload; + if (length == 0) return; + scanner->ending_char = buffer[0]; + if (length == 1) return; + scanner->level_count = buffer[1]; +} + +static bool scan_block_start(Scanner *scanner, TSLexer *lexer) { + if (consume_char('[', lexer)) { + uint8_t level = consume_and_count_char('=', lexer); + + if (consume_char('[', lexer)) { + scanner->level_count = level; + return true; + } + } + + return false; +} + +static bool scan_block_end(Scanner *scanner, TSLexer *lexer) { + if (consume_char(']', lexer)) { + uint8_t level = consume_and_count_char('=', lexer); + + if (scanner->level_count == level && consume_char(']', lexer)) { + return true; + } + } + + return false; +} + +static bool scan_block_content(Scanner *scanner, TSLexer *lexer) { + while (lexer->lookahead != 0) { + if (lexer->lookahead == ']') { + lexer->mark_end(lexer); + + if (scan_block_end(scanner, lexer)) { + return true; + } + } else { + consume(lexer); + } + } + + return false; +} + +static bool scan_comment_start(Scanner *scanner, TSLexer *lexer) { + if (consume_char('-', lexer) && consume_char('-', lexer)) { + lexer->mark_end(lexer); + + if (scan_block_start(scanner, lexer)) { + lexer->mark_end(lexer); + lexer->result_symbol = BLOCK_COMMENT_START; + return true; + } + } + + return false; +} + +static bool scan_comment_content(Scanner *scanner, TSLexer *lexer) { + if (scanner->ending_char == 0) { // block comment + if (scan_block_content(scanner, lexer)) { + lexer->result_symbol = BLOCK_COMMENT_CONTENT; + return true; + } + + return false; + } + + while (lexer->lookahead != 0) { + if (lexer->lookahead == scanner->ending_char) { + reset_state(scanner); + lexer->result_symbol = BLOCK_COMMENT_CONTENT; + return true; + } + + consume(lexer); + } + + return false; +} + +bool tree_sitter_lua_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { + Scanner *scanner = (Scanner *)payload; + + if (valid_symbols[BLOCK_STRING_END] && scan_block_end(scanner, lexer)) { + reset_state(scanner); + lexer->result_symbol = BLOCK_STRING_END; + return true; + } + + if (valid_symbols[BLOCK_STRING_CONTENT] && scan_block_content(scanner, lexer)) { + lexer->result_symbol = BLOCK_STRING_CONTENT; + return true; + } + + if (valid_symbols[BLOCK_COMMENT_END] && scanner->ending_char == 0 && scan_block_end(scanner, lexer)) { + reset_state(scanner); + lexer->result_symbol = BLOCK_COMMENT_END; + return true; + } + + if (valid_symbols[BLOCK_COMMENT_CONTENT] && scan_comment_content(scanner, lexer)) { + return true; + } + + skip_whitespaces(lexer); + + if (valid_symbols[BLOCK_STRING_START] && scan_block_start(scanner, lexer)) { + lexer->result_symbol = BLOCK_STRING_START; + return true; + } + + if (valid_symbols[BLOCK_COMMENT_START]) { + if (scan_comment_start(scanner, lexer)) { + return true; + } + } + + return false; +} diff --git a/Packages/Sources/TreeSitterScanners/csrc/python_scanner.c b/Packages/Sources/TreeSitterScanners/csrc/python_scanner.c new file mode 100644 index 00000000..1fc77cdb --- /dev/null +++ b/Packages/Sources/TreeSitterScanners/csrc/python_scanner.c @@ -0,0 +1,437 @@ +#include "tree_sitter/array.h" +#include "tree_sitter/parser.h" + +#include +#include +#include +#include + +enum TokenType { + NEWLINE, + INDENT, + DEDENT, + STRING_START, + STRING_CONTENT, + ESCAPE_INTERPOLATION, + STRING_END, + COMMENT, + CLOSE_PAREN, + CLOSE_BRACKET, + CLOSE_BRACE, + EXCEPT, +}; + +typedef enum { + SingleQuote = 1 << 0, + DoubleQuote = 1 << 1, + BackQuote = 1 << 2, + Raw = 1 << 3, + Format = 1 << 4, + Triple = 1 << 5, + Bytes = 1 << 6, +} Flags; + +typedef struct { + char flags; +} Delimiter; + +static inline Delimiter new_delimiter() { return (Delimiter){0}; } + +static inline bool is_format(Delimiter *delimiter) { return delimiter->flags & Format; } + +static inline bool is_raw(Delimiter *delimiter) { return delimiter->flags & Raw; } + +static inline bool is_triple(Delimiter *delimiter) { return delimiter->flags & Triple; } + +static inline bool is_bytes(Delimiter *delimiter) { return delimiter->flags & Bytes; } + +static inline int32_t end_character(Delimiter *delimiter) { + if (delimiter->flags & SingleQuote) { + return '\''; + } + if (delimiter->flags & DoubleQuote) { + return '"'; + } + if (delimiter->flags & BackQuote) { + return '`'; + } + return 0; +} + +static inline void set_format(Delimiter *delimiter) { delimiter->flags |= Format; } + +static inline void set_raw(Delimiter *delimiter) { delimiter->flags |= Raw; } + +static inline void set_triple(Delimiter *delimiter) { delimiter->flags |= Triple; } + +static inline void set_bytes(Delimiter *delimiter) { delimiter->flags |= Bytes; } + +static inline void set_end_character(Delimiter *delimiter, int32_t character) { + switch (character) { + case '\'': + delimiter->flags |= SingleQuote; + break; + case '"': + delimiter->flags |= DoubleQuote; + break; + case '`': + delimiter->flags |= BackQuote; + break; + default: + assert(false); + } +} + +typedef struct { + Array(uint16_t) indents; + Array(Delimiter) delimiters; + bool inside_interpolated_string; +} Scanner; + +static inline void advance(TSLexer *lexer) { lexer->advance(lexer, false); } + +static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } + +bool tree_sitter_python_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { + Scanner *scanner = (Scanner *)payload; + + bool error_recovery_mode = valid_symbols[STRING_CONTENT] && valid_symbols[INDENT]; + bool within_brackets = valid_symbols[CLOSE_BRACE] || valid_symbols[CLOSE_PAREN] || valid_symbols[CLOSE_BRACKET]; + + bool advanced_once = false; + if (valid_symbols[ESCAPE_INTERPOLATION] && scanner->delimiters.size > 0 && + (lexer->lookahead == '{' || lexer->lookahead == '}') && !error_recovery_mode) { + Delimiter *delimiter = array_back(&scanner->delimiters); + if (is_format(delimiter)) { + lexer->mark_end(lexer); + bool is_left_brace = lexer->lookahead == '{'; + advance(lexer); + advanced_once = true; + if ((lexer->lookahead == '{' && is_left_brace) || (lexer->lookahead == '}' && !is_left_brace)) { + advance(lexer); + lexer->mark_end(lexer); + lexer->result_symbol = ESCAPE_INTERPOLATION; + return true; + } + return false; + } + } + + if (valid_symbols[STRING_CONTENT] && scanner->delimiters.size > 0 && !error_recovery_mode) { + Delimiter *delimiter = array_back(&scanner->delimiters); + int32_t end_char = end_character(delimiter); + bool has_content = advanced_once; + while (lexer->lookahead) { + if ((advanced_once || lexer->lookahead == '{' || lexer->lookahead == '}') && is_format(delimiter)) { + lexer->mark_end(lexer); + lexer->result_symbol = STRING_CONTENT; + return has_content; + } + if (lexer->lookahead == '\\') { + if (is_raw(delimiter)) { + // Step over the backslash. + advance(lexer); + // Step over any escaped quotes. + if (lexer->lookahead == end_character(delimiter) || lexer->lookahead == '\\') { + advance(lexer); + } + // Step over newlines + if (lexer->lookahead == '\r') { + advance(lexer); + if (lexer->lookahead == '\n') { + advance(lexer); + } + } else if (lexer->lookahead == '\n') { + advance(lexer); + } + continue; + } + if (is_bytes(delimiter)) { + lexer->mark_end(lexer); + advance(lexer); + if (lexer->lookahead == 'N' || lexer->lookahead == 'u' || lexer->lookahead == 'U') { + // In bytes string, \N{...}, \uXXXX and \UXXXXXXXX are + // not escape sequences + // https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + advance(lexer); + } else { + lexer->result_symbol = STRING_CONTENT; + return has_content; + } + } else { + lexer->mark_end(lexer); + lexer->result_symbol = STRING_CONTENT; + return has_content; + } + } else if (lexer->lookahead == end_char) { + if (is_triple(delimiter)) { + lexer->mark_end(lexer); + advance(lexer); + if (lexer->lookahead == end_char) { + advance(lexer); + if (lexer->lookahead == end_char) { + if (has_content) { + lexer->result_symbol = STRING_CONTENT; + } else { + advance(lexer); + lexer->mark_end(lexer); + array_pop(&scanner->delimiters); + lexer->result_symbol = STRING_END; + scanner->inside_interpolated_string = false; + } + return true; + } + lexer->mark_end(lexer); + lexer->result_symbol = STRING_CONTENT; + return true; + } + lexer->mark_end(lexer); + lexer->result_symbol = STRING_CONTENT; + return true; + } + if (has_content) { + lexer->result_symbol = STRING_CONTENT; + } else { + advance(lexer); + array_pop(&scanner->delimiters); + lexer->result_symbol = STRING_END; + scanner->inside_interpolated_string = false; + } + lexer->mark_end(lexer); + return true; + + } else if (lexer->lookahead == '\n' && has_content && !is_triple(delimiter)) { + return false; + } + advance(lexer); + has_content = true; + } + } + + lexer->mark_end(lexer); + + bool found_end_of_line = false; + uint16_t indent_length = 0; + int32_t first_comment_indent_length = -1; + for (;;) { + if (lexer->lookahead == '\n') { + found_end_of_line = true; + indent_length = 0; + skip(lexer); + } else if (lexer->lookahead == ' ') { + indent_length++; + skip(lexer); + } else if (lexer->lookahead == '\r' || lexer->lookahead == '\f') { + indent_length = 0; + skip(lexer); + } else if (lexer->lookahead == '\t') { + indent_length += 8; + skip(lexer); + } else if (lexer->lookahead == '#' && (valid_symbols[INDENT] || valid_symbols[DEDENT] || + valid_symbols[NEWLINE] || valid_symbols[EXCEPT])) { + // If we haven't found an EOL yet, + // then this is a comment after an expression: + // foo = bar # comment + // Just return, since we don't want to generate an indent/dedent + // token. + if (!found_end_of_line) { + return false; + } + if (first_comment_indent_length == -1) { + first_comment_indent_length = (int32_t)indent_length; + } + while (lexer->lookahead && lexer->lookahead != '\n') { + skip(lexer); + } + skip(lexer); + indent_length = 0; + } else if (lexer->lookahead == '\\') { + skip(lexer); + if (lexer->lookahead == '\r') { + skip(lexer); + } + if (lexer->lookahead == '\n' || lexer->eof(lexer)) { + skip(lexer); + } else { + return false; + } + } else if (lexer->eof(lexer)) { + indent_length = 0; + found_end_of_line = true; + break; + } else { + break; + } + } + + if (found_end_of_line) { + if (scanner->indents.size > 0) { + uint16_t current_indent_length = *array_back(&scanner->indents); + + if (valid_symbols[INDENT] && indent_length > current_indent_length) { + array_push(&scanner->indents, indent_length); + lexer->result_symbol = INDENT; + return true; + } + + bool next_tok_is_string_start = + lexer->lookahead == '\"' || lexer->lookahead == '\'' || lexer->lookahead == '`'; + + if ((valid_symbols[DEDENT] || + (!valid_symbols[NEWLINE] && !(valid_symbols[STRING_START] && next_tok_is_string_start) && + !within_brackets)) && + indent_length < current_indent_length && !scanner->inside_interpolated_string && + + // Wait to create a dedent token until we've consumed any + // comments + // whose indentation matches the current block. + first_comment_indent_length < (int32_t)current_indent_length) { + array_pop(&scanner->indents); + lexer->result_symbol = DEDENT; + return true; + } + } + + if (valid_symbols[NEWLINE] && !error_recovery_mode) { + lexer->result_symbol = NEWLINE; + return true; + } + } + + if (first_comment_indent_length == -1 && valid_symbols[STRING_START]) { + Delimiter delimiter = new_delimiter(); + + bool has_flags = false; + while (lexer->lookahead) { + if (lexer->lookahead == 'f' || lexer->lookahead == 'F' || lexer->lookahead == 't' || + lexer->lookahead == 'T') { + set_format(&delimiter); + } else if (lexer->lookahead == 'r' || lexer->lookahead == 'R') { + set_raw(&delimiter); + } else if (lexer->lookahead == 'b' || lexer->lookahead == 'B') { + set_bytes(&delimiter); + } else if (lexer->lookahead != 'u' && lexer->lookahead != 'U') { + break; + } + has_flags = true; + advance(lexer); + } + + if (lexer->lookahead == '`') { + set_end_character(&delimiter, '`'); + advance(lexer); + lexer->mark_end(lexer); + } else if (lexer->lookahead == '\'') { + set_end_character(&delimiter, '\''); + advance(lexer); + lexer->mark_end(lexer); + if (lexer->lookahead == '\'') { + advance(lexer); + if (lexer->lookahead == '\'') { + advance(lexer); + lexer->mark_end(lexer); + set_triple(&delimiter); + } + } + } else if (lexer->lookahead == '"') { + set_end_character(&delimiter, '"'); + advance(lexer); + lexer->mark_end(lexer); + if (lexer->lookahead == '"') { + advance(lexer); + if (lexer->lookahead == '"') { + advance(lexer); + lexer->mark_end(lexer); + set_triple(&delimiter); + } + } + } + + if (end_character(&delimiter)) { + array_push(&scanner->delimiters, delimiter); + lexer->result_symbol = STRING_START; + scanner->inside_interpolated_string = is_format(&delimiter); + return true; + } + if (has_flags) { + return false; + } + } + + return false; +} + +unsigned tree_sitter_python_external_scanner_serialize(void *payload, char *buffer) { + Scanner *scanner = (Scanner *)payload; + + size_t size = 0; + + buffer[size++] = (char)scanner->inside_interpolated_string; + + size_t delimiter_count = scanner->delimiters.size; + if (delimiter_count > UINT8_MAX) { + delimiter_count = UINT8_MAX; + } + buffer[size++] = (char)delimiter_count; + + if (delimiter_count > 0) { + memcpy(&buffer[size], scanner->delimiters.contents, delimiter_count); + } + size += delimiter_count; + + uint32_t iter = 1; + for (; iter < scanner->indents.size && size < TREE_SITTER_SERIALIZATION_BUFFER_SIZE; ++iter) { + uint16_t indent_value = *array_get(&scanner->indents, iter); + buffer[size++] = (char)(indent_value & 0xFF); + buffer[size++] = (char)((indent_value >> 8) & 0xFF); + } + + return size; +} + +void tree_sitter_python_external_scanner_deserialize(void *payload, const char *buffer, unsigned length) { + Scanner *scanner = (Scanner *)payload; + + array_delete(&scanner->delimiters); + array_delete(&scanner->indents); + array_push(&scanner->indents, 0); + + if (length > 0) { + size_t size = 0; + + scanner->inside_interpolated_string = (bool)buffer[size++]; + + size_t delimiter_count = (uint8_t)buffer[size++]; + if (delimiter_count > 0) { + array_reserve(&scanner->delimiters, delimiter_count); + scanner->delimiters.size = delimiter_count; + memcpy(scanner->delimiters.contents, &buffer[size], delimiter_count); + size += delimiter_count; + } + + for (; size + 1 < length; size += 2) { + uint16_t indent_value = (unsigned char)buffer[size] | ((unsigned char)buffer[size + 1] << 8); + array_push(&scanner->indents, indent_value); + } + } +} + +void *tree_sitter_python_external_scanner_create() { +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) + _Static_assert(sizeof(Delimiter) == sizeof(char), ""); +#else + assert(sizeof(Delimiter) == sizeof(char)); +#endif + Scanner *scanner = calloc(1, sizeof(Scanner)); + array_init(&scanner->indents); + array_init(&scanner->delimiters); + tree_sitter_python_external_scanner_deserialize(scanner, NULL, 0); + return scanner; +} + +void tree_sitter_python_external_scanner_destroy(void *payload) { + Scanner *scanner = (Scanner *)payload; + array_delete(&scanner->indents); + array_delete(&scanner->delimiters); + free(scanner); +} diff --git a/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/alloc.h b/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/alloc.h new file mode 100644 index 00000000..1abdd120 --- /dev/null +++ b/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/alloc.h @@ -0,0 +1,54 @@ +#ifndef TREE_SITTER_ALLOC_H_ +#define TREE_SITTER_ALLOC_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +// Allow clients to override allocation functions +#ifdef TREE_SITTER_REUSE_ALLOCATOR + +extern void *(*ts_current_malloc)(size_t size); +extern void *(*ts_current_calloc)(size_t count, size_t size); +extern void *(*ts_current_realloc)(void *ptr, size_t size); +extern void (*ts_current_free)(void *ptr); + +#ifndef ts_malloc +#define ts_malloc ts_current_malloc +#endif +#ifndef ts_calloc +#define ts_calloc ts_current_calloc +#endif +#ifndef ts_realloc +#define ts_realloc ts_current_realloc +#endif +#ifndef ts_free +#define ts_free ts_current_free +#endif + +#else + +#ifndef ts_malloc +#define ts_malloc malloc +#endif +#ifndef ts_calloc +#define ts_calloc calloc +#endif +#ifndef ts_realloc +#define ts_realloc realloc +#endif +#ifndef ts_free +#define ts_free free +#endif + +#endif + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_ALLOC_H_ diff --git a/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/array.h b/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/array.h new file mode 100644 index 00000000..a17a574f --- /dev/null +++ b/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/array.h @@ -0,0 +1,291 @@ +#ifndef TREE_SITTER_ARRAY_H_ +#define TREE_SITTER_ARRAY_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "./alloc.h" + +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4101) +#elif defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#endif + +#define Array(T) \ + struct { \ + T *contents; \ + uint32_t size; \ + uint32_t capacity; \ + } + +/// Initialize an array. +#define array_init(self) \ + ((self)->size = 0, (self)->capacity = 0, (self)->contents = NULL) + +/// Create an empty array. +#define array_new() \ + { NULL, 0, 0 } + +/// Get a pointer to the element at a given `index` in the array. +#define array_get(self, _index) \ + (assert((uint32_t)(_index) < (self)->size), &(self)->contents[_index]) + +/// Get a pointer to the first element in the array. +#define array_front(self) array_get(self, 0) + +/// Get a pointer to the last element in the array. +#define array_back(self) array_get(self, (self)->size - 1) + +/// Clear the array, setting its size to zero. Note that this does not free any +/// memory allocated for the array's contents. +#define array_clear(self) ((self)->size = 0) + +/// Reserve `new_capacity` elements of space in the array. If `new_capacity` is +/// less than the array's current capacity, this function has no effect. +#define array_reserve(self, new_capacity) \ + _array__reserve((Array *)(self), array_elem_size(self), new_capacity) + +/// Free any memory allocated for this array. Note that this does not free any +/// memory allocated for the array's contents. +#define array_delete(self) _array__delete((Array *)(self)) + +/// Push a new `element` onto the end of the array. +#define array_push(self, element) \ + (_array__grow((Array *)(self), 1, array_elem_size(self)), \ + (self)->contents[(self)->size++] = (element)) + +/// Increase the array's size by `count` elements. +/// New elements are zero-initialized. +#define array_grow_by(self, count) \ + do { \ + if ((count) == 0) break; \ + _array__grow((Array *)(self), count, array_elem_size(self)); \ + memset((self)->contents + (self)->size, 0, (count) * array_elem_size(self)); \ + (self)->size += (count); \ + } while (0) + +/// Append all elements from one array to the end of another. +#define array_push_all(self, other) \ + array_extend((self), (other)->size, (other)->contents) + +/// Append `count` elements to the end of the array, reading their values from the +/// `contents` pointer. +#define array_extend(self, count, contents) \ + _array__splice( \ + (Array *)(self), array_elem_size(self), (self)->size, \ + 0, count, contents \ + ) + +/// Remove `old_count` elements from the array starting at the given `index`. At +/// the same index, insert `new_count` new elements, reading their values from the +/// `new_contents` pointer. +#define array_splice(self, _index, old_count, new_count, new_contents) \ + _array__splice( \ + (Array *)(self), array_elem_size(self), _index, \ + old_count, new_count, new_contents \ + ) + +/// Insert one `element` into the array at the given `index`. +#define array_insert(self, _index, element) \ + _array__splice((Array *)(self), array_elem_size(self), _index, 0, 1, &(element)) + +/// Remove one element from the array at the given `index`. +#define array_erase(self, _index) \ + _array__erase((Array *)(self), array_elem_size(self), _index) + +/// Pop the last element off the array, returning the element by value. +#define array_pop(self) ((self)->contents[--(self)->size]) + +/// Assign the contents of one array to another, reallocating if necessary. +#define array_assign(self, other) \ + _array__assign((Array *)(self), (const Array *)(other), array_elem_size(self)) + +/// Swap one array with another +#define array_swap(self, other) \ + _array__swap((Array *)(self), (Array *)(other)) + +/// Get the size of the array contents +#define array_elem_size(self) (sizeof *(self)->contents) + +/// Search a sorted array for a given `needle` value, using the given `compare` +/// callback to determine the order. +/// +/// If an existing element is found to be equal to `needle`, then the `index` +/// out-parameter is set to the existing value's index, and the `exists` +/// out-parameter is set to true. Otherwise, `index` is set to an index where +/// `needle` should be inserted in order to preserve the sorting, and `exists` +/// is set to false. +#define array_search_sorted_with(self, compare, needle, _index, _exists) \ + _array__search_sorted(self, 0, compare, , needle, _index, _exists) + +/// Search a sorted array for a given `needle` value, using integer comparisons +/// of a given struct field (specified with a leading dot) to determine the order. +/// +/// See also `array_search_sorted_with`. +#define array_search_sorted_by(self, field, needle, _index, _exists) \ + _array__search_sorted(self, 0, _compare_int, field, needle, _index, _exists) + +/// Insert a given `value` into a sorted array, using the given `compare` +/// callback to determine the order. +#define array_insert_sorted_with(self, compare, value) \ + do { \ + unsigned _index, _exists; \ + array_search_sorted_with(self, compare, &(value), &_index, &_exists); \ + if (!_exists) array_insert(self, _index, value); \ + } while (0) + +/// Insert a given `value` into a sorted array, using integer comparisons of +/// a given struct field (specified with a leading dot) to determine the order. +/// +/// See also `array_search_sorted_by`. +#define array_insert_sorted_by(self, field, value) \ + do { \ + unsigned _index, _exists; \ + array_search_sorted_by(self, field, (value) field, &_index, &_exists); \ + if (!_exists) array_insert(self, _index, value); \ + } while (0) + +// Private + +typedef Array(void) Array; + +/// This is not what you're looking for, see `array_delete`. +static inline void _array__delete(Array *self) { + if (self->contents) { + ts_free(self->contents); + self->contents = NULL; + self->size = 0; + self->capacity = 0; + } +} + +/// This is not what you're looking for, see `array_erase`. +static inline void _array__erase(Array *self, size_t element_size, + uint32_t index) { + assert(index < self->size); + char *contents = (char *)self->contents; + memmove(contents + index * element_size, contents + (index + 1) * element_size, + (self->size - index - 1) * element_size); + self->size--; +} + +/// This is not what you're looking for, see `array_reserve`. +static inline void _array__reserve(Array *self, size_t element_size, uint32_t new_capacity) { + if (new_capacity > self->capacity) { + if (self->contents) { + self->contents = ts_realloc(self->contents, new_capacity * element_size); + } else { + self->contents = ts_malloc(new_capacity * element_size); + } + self->capacity = new_capacity; + } +} + +/// This is not what you're looking for, see `array_assign`. +static inline void _array__assign(Array *self, const Array *other, size_t element_size) { + _array__reserve(self, element_size, other->size); + self->size = other->size; + memcpy(self->contents, other->contents, self->size * element_size); +} + +/// This is not what you're looking for, see `array_swap`. +static inline void _array__swap(Array *self, Array *other) { + Array swap = *other; + *other = *self; + *self = swap; +} + +/// This is not what you're looking for, see `array_push` or `array_grow_by`. +static inline void _array__grow(Array *self, uint32_t count, size_t element_size) { + uint32_t new_size = self->size + count; + if (new_size > self->capacity) { + uint32_t new_capacity = self->capacity * 2; + if (new_capacity < 8) new_capacity = 8; + if (new_capacity < new_size) new_capacity = new_size; + _array__reserve(self, element_size, new_capacity); + } +} + +/// This is not what you're looking for, see `array_splice`. +static inline void _array__splice(Array *self, size_t element_size, + uint32_t index, uint32_t old_count, + uint32_t new_count, const void *elements) { + uint32_t new_size = self->size + new_count - old_count; + uint32_t old_end = index + old_count; + uint32_t new_end = index + new_count; + assert(old_end <= self->size); + + _array__reserve(self, element_size, new_size); + + char *contents = (char *)self->contents; + if (self->size > old_end) { + memmove( + contents + new_end * element_size, + contents + old_end * element_size, + (self->size - old_end) * element_size + ); + } + if (new_count > 0) { + if (elements) { + memcpy( + (contents + index * element_size), + elements, + new_count * element_size + ); + } else { + memset( + (contents + index * element_size), + 0, + new_count * element_size + ); + } + } + self->size += new_count - old_count; +} + +/// A binary search routine, based on Rust's `std::slice::binary_search_by`. +/// This is not what you're looking for, see `array_search_sorted_with` or `array_search_sorted_by`. +#define _array__search_sorted(self, start, compare, suffix, needle, _index, _exists) \ + do { \ + *(_index) = start; \ + *(_exists) = false; \ + uint32_t size = (self)->size - *(_index); \ + if (size == 0) break; \ + int comparison; \ + while (size > 1) { \ + uint32_t half_size = size / 2; \ + uint32_t mid_index = *(_index) + half_size; \ + comparison = compare(&((self)->contents[mid_index] suffix), (needle)); \ + if (comparison <= 0) *(_index) = mid_index; \ + size -= half_size; \ + } \ + comparison = compare(&((self)->contents[*(_index)] suffix), (needle)); \ + if (comparison == 0) *(_exists) = true; \ + else if (comparison < 0) *(_index) += 1; \ + } while (0) + +/// Helper macro for the `_sorted_by` routines below. This takes the left (existing) +/// parameter by reference in order to work with the generic sorting function above. +#define _compare_int(a, b) ((int)*(a) - (int)(b)) + +#ifdef _MSC_VER +#pragma warning(pop) +#elif defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_ARRAY_H_ diff --git a/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/parser.h b/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/parser.h new file mode 100644 index 00000000..858107de --- /dev/null +++ b/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/parser.h @@ -0,0 +1,286 @@ +#ifndef TREE_SITTER_PARSER_H_ +#define TREE_SITTER_PARSER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#define ts_builtin_sym_error ((TSSymbol)-1) +#define ts_builtin_sym_end 0 +#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024 + +#ifndef TREE_SITTER_API_H_ +typedef uint16_t TSStateId; +typedef uint16_t TSSymbol; +typedef uint16_t TSFieldId; +typedef struct TSLanguage TSLanguage; +typedef struct TSLanguageMetadata { + uint8_t major_version; + uint8_t minor_version; + uint8_t patch_version; +} TSLanguageMetadata; +#endif + +typedef struct { + TSFieldId field_id; + uint8_t child_index; + bool inherited; +} TSFieldMapEntry; + +// Used to index the field and supertype maps. +typedef struct { + uint16_t index; + uint16_t length; +} TSMapSlice; + +typedef struct { + bool visible; + bool named; + bool supertype; +} TSSymbolMetadata; + +typedef struct TSLexer TSLexer; + +struct TSLexer { + int32_t lookahead; + TSSymbol result_symbol; + void (*advance)(TSLexer *, bool); + void (*mark_end)(TSLexer *); + uint32_t (*get_column)(TSLexer *); + bool (*is_at_included_range_start)(const TSLexer *); + bool (*eof)(const TSLexer *); + void (*log)(const TSLexer *, const char *, ...); +}; + +typedef enum { + TSParseActionTypeShift, + TSParseActionTypeReduce, + TSParseActionTypeAccept, + TSParseActionTypeRecover, +} TSParseActionType; + +typedef union { + struct { + uint8_t type; + TSStateId state; + bool extra; + bool repetition; + } shift; + struct { + uint8_t type; + uint8_t child_count; + TSSymbol symbol; + int16_t dynamic_precedence; + uint16_t production_id; + } reduce; + uint8_t type; +} TSParseAction; + +typedef struct { + uint16_t lex_state; + uint16_t external_lex_state; +} TSLexMode; + +typedef struct { + uint16_t lex_state; + uint16_t external_lex_state; + uint16_t reserved_word_set_id; +} TSLexerMode; + +typedef union { + TSParseAction action; + struct { + uint8_t count; + bool reusable; + } entry; +} TSParseActionEntry; + +typedef struct { + int32_t start; + int32_t end; +} TSCharacterRange; + +struct TSLanguage { + uint32_t abi_version; + uint32_t symbol_count; + uint32_t alias_count; + uint32_t token_count; + uint32_t external_token_count; + uint32_t state_count; + uint32_t large_state_count; + uint32_t production_id_count; + uint32_t field_count; + uint16_t max_alias_sequence_length; + const uint16_t *parse_table; + const uint16_t *small_parse_table; + const uint32_t *small_parse_table_map; + const TSParseActionEntry *parse_actions; + const char * const *symbol_names; + const char * const *field_names; + const TSMapSlice *field_map_slices; + const TSFieldMapEntry *field_map_entries; + const TSSymbolMetadata *symbol_metadata; + const TSSymbol *public_symbol_map; + const uint16_t *alias_map; + const TSSymbol *alias_sequences; + const TSLexerMode *lex_modes; + bool (*lex_fn)(TSLexer *, TSStateId); + bool (*keyword_lex_fn)(TSLexer *, TSStateId); + TSSymbol keyword_capture_token; + struct { + const bool *states; + const TSSymbol *symbol_map; + void *(*create)(void); + void (*destroy)(void *); + bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist); + unsigned (*serialize)(void *, char *); + void (*deserialize)(void *, const char *, unsigned); + } external_scanner; + const TSStateId *primary_state_ids; + const char *name; + const TSSymbol *reserved_words; + uint16_t max_reserved_word_set_size; + uint32_t supertype_count; + const TSSymbol *supertype_symbols; + const TSMapSlice *supertype_map_slices; + const TSSymbol *supertype_map_entries; + TSLanguageMetadata metadata; +}; + +static inline bool set_contains(const TSCharacterRange *ranges, uint32_t len, int32_t lookahead) { + uint32_t index = 0; + uint32_t size = len - index; + while (size > 1) { + uint32_t half_size = size / 2; + uint32_t mid_index = index + half_size; + const TSCharacterRange *range = &ranges[mid_index]; + if (lookahead >= range->start && lookahead <= range->end) { + return true; + } else if (lookahead > range->end) { + index = mid_index; + } + size -= half_size; + } + const TSCharacterRange *range = &ranges[index]; + return (lookahead >= range->start && lookahead <= range->end); +} + +/* + * Lexer Macros + */ + +#ifdef _MSC_VER +#define UNUSED __pragma(warning(suppress : 4101)) +#else +#define UNUSED __attribute__((unused)) +#endif + +#define START_LEXER() \ + bool result = false; \ + bool skip = false; \ + UNUSED \ + bool eof = false; \ + int32_t lookahead; \ + goto start; \ + next_state: \ + lexer->advance(lexer, skip); \ + start: \ + skip = false; \ + lookahead = lexer->lookahead; + +#define ADVANCE(state_value) \ + { \ + state = state_value; \ + goto next_state; \ + } + +#define ADVANCE_MAP(...) \ + { \ + static const uint16_t map[] = { __VA_ARGS__ }; \ + for (uint32_t i = 0; i < sizeof(map) / sizeof(map[0]); i += 2) { \ + if (map[i] == lookahead) { \ + state = map[i + 1]; \ + goto next_state; \ + } \ + } \ + } + +#define SKIP(state_value) \ + { \ + skip = true; \ + state = state_value; \ + goto next_state; \ + } + +#define ACCEPT_TOKEN(symbol_value) \ + result = true; \ + lexer->result_symbol = symbol_value; \ + lexer->mark_end(lexer); + +#define END_STATE() return result; + +/* + * Parse Table Macros + */ + +#define SMALL_STATE(id) ((id) - LARGE_STATE_COUNT) + +#define STATE(id) id + +#define ACTIONS(id) id + +#define SHIFT(state_value) \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .state = (state_value) \ + } \ + }} + +#define SHIFT_REPEAT(state_value) \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .state = (state_value), \ + .repetition = true \ + } \ + }} + +#define SHIFT_EXTRA() \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .extra = true \ + } \ + }} + +#define REDUCE(symbol_name, children, precedence, prod_id) \ + {{ \ + .reduce = { \ + .type = TSParseActionTypeReduce, \ + .symbol = symbol_name, \ + .child_count = children, \ + .dynamic_precedence = precedence, \ + .production_id = prod_id \ + }, \ + }} + +#define RECOVER() \ + {{ \ + .type = TSParseActionTypeRecover \ + }} + +#define ACCEPT_INPUT() \ + {{ \ + .type = TSParseActionTypeAccept \ + }} + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_PARSER_H_ diff --git a/Packages/Sources/TreeSitterScanners/include/treesitter_scanners.h b/Packages/Sources/TreeSitterScanners/include/treesitter_scanners.h new file mode 100644 index 00000000..6750bb86 --- /dev/null +++ b/Packages/Sources/TreeSitterScanners/include/treesitter_scanners.h @@ -0,0 +1,2 @@ +// Intentionally empty. This target only provides external-scanner +// symbols for grammar packages whose SPM manifest omits scanner.c. diff --git a/Packages/Tests/RxCodeEditorTests/PredefinedAutocompleteProviderTests.swift b/Packages/Tests/RxCodeEditorTests/PredefinedAutocompleteProviderTests.swift new file mode 100644 index 00000000..5751eb47 --- /dev/null +++ b/Packages/Tests/RxCodeEditorTests/PredefinedAutocompleteProviderTests.swift @@ -0,0 +1,72 @@ +import Foundation +import RxCodeEditor +import Testing + +@Suite("Predefined autocomplete provider") +struct PredefinedAutocompleteProviderTests { + @Test("Returns all placeholders at an empty trigger") + func emptyTriggerReturnsAllPlaceholders() throws { + let provider = PredefinedAutocompleteProvider.placeholders(["projectName", "branch"]) + let text = #"{"body":"{{"}"# + let location = (text as NSString).length - 2 + + let context = try #require(provider.context( + in: text, + selectedRange: NSRange(location: location, length: 0) + )) + + #expect(context.query.isEmpty) + #expect(context.replacementRange == NSRange(location: 9, length: 2)) + #expect(provider.completions(for: context).map(\.insertionText) == [ + "{{projectName}}", + "{{branch}}", + ]) + } + + @Test("Filters placeholders by typed query") + func filtersByQuery() throws { + let provider = PredefinedAutocompleteProvider.placeholders([ + "projectName", + "projectPath", + "gitHubRepo", + "branch", + ]) + let text = #"{"body":"{{proj"}"# + let location = (text as NSString).length - 2 + + let context = try #require(provider.context( + in: text, + selectedRange: NSRange(location: location, length: 0) + )) + + #expect(context.query == "proj") + #expect(provider.completions(for: context).map(\.insertionText) == [ + "{{projectName}}", + "{{projectPath}}", + ]) + } + + @Test("Does not offer completions after a closed placeholder") + func ignoresClosedPlaceholder() { + let provider = PredefinedAutocompleteProvider.placeholders(["projectName"]) + let text = #"{"body":"{{projectName}}"}"# + let location = (text as NSString).length - 2 + + #expect(provider.context( + in: text, + selectedRange: NSRange(location: location, length: 0) + ) == nil) + } + + @Test("Does not offer completions for whitespace queries") + func ignoresWhitespaceQuery() { + let provider = PredefinedAutocompleteProvider.placeholders(["projectName"]) + let text = #"{"body":"{{project name"}"# + let location = (text as NSString).length - 2 + + #expect(provider.context( + in: text, + selectedRange: NSRange(location: location, length: 0) + ) == nil) + } +} diff --git a/RxCode.xcodeproj/project.pbxproj b/RxCode.xcodeproj/project.pbxproj index 345aa39b..c22bd56e 100644 --- a/RxCode.xcodeproj/project.pbxproj +++ b/RxCode.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ E6C001022F9B000100000001 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E6C001012F9B000100000001 /* Sparkle */; }; E6D001032FA0000100000001 /* RxCodeCore in Frameworks */ = {isa = PBXBuildFile; productRef = E6D001012FA0000100000001 /* RxCodeCore */; }; E6D001042FA0000100000001 /* RxCodeChatKit in Frameworks */ = {isa = PBXBuildFile; productRef = E6D001022FA0000100000001 /* RxCodeChatKit */; }; + E6D001052FA0000100000001 /* RxCodeEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E6D001082FA0000100000001 /* RxCodeEditor */; }; E6D001072FA0000100000001 /* RxCodeSync in Frameworks */ = {isa = PBXBuildFile; productRef = E6D001062FA0000100000001 /* RxCodeSync */; }; FB0000030000000000000001 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB0000010000000000000001 /* FirebaseAnalytics */; }; FB0000040000000000000001 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB0000020000000000000001 /* FirebaseCrashlytics */; }; @@ -315,10 +316,11 @@ DF46526A2FCD34B3002D9562 /* JSONSchemaValidator in Frameworks */, DFAA00012FCD34B3002D9562 /* JSONSchema in Frameworks */, DF462A622FC6EDCE002D9562 /* RxAuthSwiftUI in Frameworks */, - DF4652682FCD34B3002D9562 /* JSONSchemaForm in Frameworks */, - E6D001042FA0000100000001 /* RxCodeChatKit in Frameworks */, - DF462DCB2FC73FA8002D9562 /* RxAuthSwiftUI in Frameworks */, - E6D001072FA0000100000001 /* RxCodeSync in Frameworks */, + DF4652682FCD34B3002D9562 /* JSONSchemaForm in Frameworks */, + E6D001042FA0000100000001 /* RxCodeChatKit in Frameworks */, + E6D001052FA0000100000001 /* RxCodeEditor in Frameworks */, + DF462DCB2FC73FA8002D9562 /* RxAuthSwiftUI in Frameworks */, + E6D001072FA0000100000001 /* RxCodeSync in Frameworks */, DF462A602FC6EDCE002D9562 /* RxAuthSwift in Frameworks */, DF23FF1D2FBB42F7008929A6 /* WaterfallGrid in Frameworks */, FB0000030000000000000001 /* FirebaseAnalytics in Frameworks */, @@ -616,10 +618,11 @@ packageProductDependencies = ( E6A001012F8A000100000001 /* SwiftTerm */, E6C001012F9B000100000001 /* Sparkle */, - E6D001012FA0000100000001 /* RxCodeCore */, - E6D001022FA0000100000001 /* RxCodeChatKit */, - E6D001062FA0000100000001 /* RxCodeSync */, - DF23FF1C2FBB42F7008929A6 /* WaterfallGrid */, + E6D001012FA0000100000001 /* RxCodeCore */, + E6D001022FA0000100000001 /* RxCodeChatKit */, + E6D001082FA0000100000001 /* RxCodeEditor */, + E6D001062FA0000100000001 /* RxCodeSync */, + DF23FF1C2FBB42F7008929A6 /* WaterfallGrid */, FB0000010000000000000001 /* FirebaseAnalytics */, FB0000020000000000000001 /* FirebaseCrashlytics */, DF4628912FC611E6002D9562 /* RxAuthSwift */, @@ -1740,6 +1743,11 @@ package = E6D001002FA0000100000001 /* XCLocalSwiftPackageReference "Packages" */; productName = RxCodeSync; }; + E6D001082FA0000100000001 /* RxCodeEditor */ = { + isa = XCSwiftPackageProductDependency; + package = E6D001002FA0000100000001 /* XCLocalSwiftPackageReference "Packages" */; + productName = RxCodeEditor; + }; FB0000010000000000000001 /* FirebaseAnalytics */ = { isa = XCSwiftPackageProductDependency; package = FB0000000000000000000001 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; diff --git a/RxCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/RxCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cfa3af20..07bfc140 100644 --- a/RxCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/RxCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -208,6 +208,15 @@ "version" : "1.38.0" } }, + { + "identity" : "swift-tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/swift-tree-sitter", + "state" : { + "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", + "version" : "0.25.0" + } + }, { "identity" : "swiftterm", "kind" : "remoteSourceControl", @@ -217,6 +226,222 @@ "version" : "1.13.0" } }, + { + "identity" : "tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter", + "state" : { + "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406", + "version" : "0.25.10" + } + }, + { + "identity" : "tree-sitter-bash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-bash", + "state" : { + "revision" : "a06c2e4415e9bc0346c6b86d401879ffb44058f7", + "version" : "0.25.1" + } + }, + { + "identity" : "tree-sitter-c", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-c", + "state" : { + "revision" : "b780e47fc780ddc8da13afa35a3f4ed5c157823d", + "version" : "0.24.2" + } + }, + { + "identity" : "tree-sitter-c-sharp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-c-sharp", + "state" : { + "revision" : "cac6d5fb595f5811a076336682d5d595ac1c9e85", + "version" : "0.23.5" + } + }, + { + "identity" : "tree-sitter-cpp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-cpp", + "state" : { + "revision" : "f41e1a044c8a84ea9fa8577fdd2eab92ec96de02", + "version" : "0.23.4" + } + }, + { + "identity" : "tree-sitter-css", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-css", + "state" : { + "revision" : "dda5cfc5722c429eaba1c910ca32c2c0c5bb1a3f", + "version" : "0.25.0" + } + }, + { + "identity" : "tree-sitter-go", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-go", + "state" : { + "revision" : "1547678a9da59885853f5f5cc8a99cc203fa2e2c", + "version" : "0.25.0" + } + }, + { + "identity" : "tree-sitter-haskell", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-haskell", + "state" : { + "revision" : "c30d812bc90827f1a54106a25bc9a6307f5cdcec", + "version" : "0.23.1" + } + }, + { + "identity" : "tree-sitter-html", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-html", + "state" : { + "revision" : "5a5ca8551a179998360b4a4ca2c0f366a35acc03", + "version" : "0.23.2" + } + }, + { + "identity" : "tree-sitter-java", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-java", + "state" : { + "revision" : "94703d5a6bed02b98e438d7cad1136c01a60ba2c", + "version" : "0.23.5" + } + }, + { + "identity" : "tree-sitter-javascript", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-javascript", + "state" : { + "revision" : "44c892e0be055ac465d5eeddae6d3e194424e7de", + "version" : "0.25.0" + } + }, + { + "identity" : "tree-sitter-json", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-json", + "state" : { + "revision" : "ee35a6ebefcef0c5c416c0d1ccec7370cfca5a24", + "version" : "0.24.8" + } + }, + { + "identity" : "tree-sitter-kotlin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/fwcd/tree-sitter-kotlin", + "state" : { + "revision" : "e1a2d5ad1f61f5740677183cd4125bb071cd2f30", + "version" : "0.3.8" + } + }, + { + "identity" : "tree-sitter-lua", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter-grammars/tree-sitter-lua", + "state" : { + "revision" : "10fe0054734eec83049514ea2e718b2a56acd0c9", + "version" : "0.5.0" + } + }, + { + "identity" : "tree-sitter-markdown", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter-grammars/tree-sitter-markdown", + "state" : { + "revision" : "f969cd3ae3f9fbd4e43205431d0ae286014c05b5", + "version" : "0.5.3" + } + }, + { + "identity" : "tree-sitter-php", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-php", + "state" : { + "revision" : "5b5627faaa290d89eb3d01b9bf47c3bb9e797dea", + "version" : "0.24.2" + } + }, + { + "identity" : "tree-sitter-python", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-python", + "state" : { + "revision" : "293fdc02038ee2bf0e2e206711b69c90ac0d413f", + "version" : "0.25.0" + } + }, + { + "identity" : "tree-sitter-ruby", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-ruby", + "state" : { + "revision" : "71bd32fb7607035768799732addba884a37a6210", + "version" : "0.23.1" + } + }, + { + "identity" : "tree-sitter-rust", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-rust", + "state" : { + "revision" : "77a3747266f4d621d0757825e6b11edcbf991ca5", + "version" : "0.24.2" + } + }, + { + "identity" : "tree-sitter-scala", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-scala", + "state" : { + "revision" : "38950b525c9dfc44c8b60d44bdd6e54217286ca8", + "version" : "0.26.0" + } + }, + { + "identity" : "tree-sitter-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/alex-pinkus/tree-sitter-swift", + "state" : { + "revision" : "31d17fe7e818a2048c808b5c6fdc2dc792f4f5b5", + "version" : "0.7.3-with-generated-files" + } + }, + { + "identity" : "tree-sitter-toml", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter-grammars/tree-sitter-toml", + "state" : { + "revision" : "64b56832c2cffe41758f28e05c756a3a98d16f41", + "version" : "0.7.0" + } + }, + { + "identity" : "tree-sitter-typescript", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter-typescript", + "state" : { + "revision" : "f975a621f4e7f532fe322e13c4f79495e0a7b2e7", + "version" : "0.23.2" + } + }, + { + "identity" : "tree-sitter-yaml", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter-grammars/tree-sitter-yaml", + "state" : { + "revision" : "b733d3f5f5005890f324333dd57e1f0badec5c87", + "version" : "0.7.0" + } + }, { "identity" : "viewinspector", "kind" : "remoteSourceControl", diff --git a/RxCode/Resources/Localizable.xcstrings b/RxCode/Resources/Localizable.xcstrings index 59c335a6..e13dbf4b 100644 --- a/RxCode/Resources/Localizable.xcstrings +++ b/RxCode/Resources/Localizable.xcstrings @@ -2780,6 +2780,7 @@ } }, "Body" : { + "extractionState" : "stale", "localizations" : { "ko" : { "stringUnit" : { @@ -2795,6 +2796,38 @@ } } }, + "Body (JSON)" : { + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "본문 (JSON)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正文 (JSON)" + } + } + } + }, + "Body must be valid JSON before it can be formatted." : { + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON 본문이 유효해야 형식을 지정할 수 있습니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON 正文必须有效才能格式化。" + } + } + } + }, "Branch" : { "localizations" : { "ko" : { @@ -6847,6 +6880,22 @@ } } }, + "Format the JSON body." : { + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON 본문의 형식을 지정합니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "格式化 JSON 正文。" + } + } + } + }, "Force off" : { "localizations" : { "ko" : { @@ -11362,6 +11411,9 @@ } } } + }, + "Pick one or more menus this item appears on." : { + }, "Pick which model performs the review — defaults to the same model as the thread." : { "localizations" : { @@ -11503,6 +11555,22 @@ } } }, + "Prettify" : { + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서식 지정" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "格式化" + } + } + } + }, "Press Command-K or use the toolbar search button to find past work across projects." : { "localizations" : { "ko" : { @@ -16900,4 +16968,4 @@ } }, "version" : "1.1" -} \ No newline at end of file +} diff --git a/RxCode/Services/Hooks/hooks/CustomMenuHook.swift b/RxCode/Services/Hooks/hooks/CustomMenuHook.swift index 0bb29f55..e8320ae4 100644 --- a/RxCode/Services/Hooks/hooks/CustomMenuHook.swift +++ b/RxCode/Services/Hooks/hooks/CustomMenuHook.swift @@ -25,7 +25,7 @@ final class CustomMenuHook: Hook { let surface: CustomMenuItemRecord.Surface = payload.branch != nil ? .briefing : .project let context = PlaceholderContext(project: payload.project, branch: payload.branch, sessionId: nil) return controller.customMenuItems(projectId: payload.project.id, surface: surface) - .map { item($0, context: context, projectId: payload.project.id) } + .map { item($0, surface: surface, context: context, projectId: payload.project.id) } } // MARK: - Thread menu @@ -33,13 +33,14 @@ final class CustomMenuHook: Hook { func onThreadContextMenu(_ payload: ThreadContextMenuPayload, controller: any HookController) -> [MenuItem] { let context = PlaceholderContext(project: payload.project, branch: nil, sessionId: payload.session.id) return controller.customMenuItems(projectId: payload.project.id, surface: .thread) - .map { item($0, context: context, projectId: payload.project.id, sessionId: payload.session.id) } + .map { item($0, surface: .thread, context: context, projectId: payload.project.id, sessionId: payload.session.id) } } // MARK: - Build one item private func item( _ record: CustomMenuItemRecord, + surface: CustomMenuItemRecord.Surface, context: PlaceholderContext, projectId: UUID, sessionId: String? = nil @@ -47,7 +48,7 @@ final class CustomMenuHook: Hook { let config = resolvedConfig(record, context: context, sessionId: sessionId) let isAPI = record.actionKindValue == .callAPI return MenuItem( - id: "\(hookID).\(record.surface).\(record.id)", + id: "\(hookID).\(surface.rawValue).\(record.id)", title: record.title, systemImage: record.systemImage, action: .command(MenuActionCommand( diff --git a/RxCode/Services/ThreadStore+CustomMenus.swift b/RxCode/Services/ThreadStore+CustomMenus.swift index 357ff23f..95286aac 100644 --- a/RxCode/Services/ThreadStore+CustomMenus.swift +++ b/RxCode/Services/ThreadStore+CustomMenus.swift @@ -20,16 +20,19 @@ extension ThreadStore { /// Enabled items that should appear on `surface` for `projectId` — i.e. items /// scoped to that project plus the "all projects" (nil) items. func customMenuItems(projectId: UUID?, surface: CustomMenuItemRecord.Surface) -> [CustomMenuItemRecord] { - let raw = surface.rawValue + // Surface membership lives in the computed `surfaces` list, so it can't be + // expressed in a #Predicate — fetch the enabled rows and filter in Swift. let descriptor = FetchDescriptor( - predicate: #Predicate { $0.isEnabled && $0.surface == raw }, + predicate: #Predicate { $0.isEnabled }, sortBy: [ SortDescriptor(\.sortOrder, order: .forward), SortDescriptor(\.createdAt, order: .forward) ] ) let rows = (try? context.fetch(descriptor)) ?? [] - return rows.filter { $0.projectId == nil || $0.projectId == projectId } + return rows.filter { + ($0.projectId == nil || $0.projectId == projectId) && $0.surfaces.contains(surface) + } } func fetchCustomMenuItem(id: String) -> CustomMenuItemRecord? { diff --git a/RxCode/Views/Settings/CustomMenuEditorSheet.swift b/RxCode/Views/Settings/CustomMenuEditorSheet.swift index a39a545a..8f0ef796 100644 --- a/RxCode/Views/Settings/CustomMenuEditorSheet.swift +++ b/RxCode/Views/Settings/CustomMenuEditorSheet.swift @@ -1,3 +1,4 @@ +import Foundation import SwiftUI import RxCodeCore @@ -9,7 +10,7 @@ struct CustomMenuDraft: Identifiable { var title: String var systemImage: String var projectId: UUID? // nil = all projects - var surface: CustomMenuItemRecord.Surface + var surfaces: Set var actionKind: CustomMenuItemRecord.ActionKind // callAPI @@ -38,7 +39,7 @@ struct CustomMenuDraft: Identifiable { title = "" systemImage = "bolt" projectId = nil - surface = .project + surfaces = [.project] actionKind = .callAPI httpMethod = "POST" urlString = "" @@ -57,7 +58,7 @@ struct CustomMenuDraft: Identifiable { title = record.title systemImage = record.systemImage ?? "bolt" projectId = record.projectId - surface = record.surfaceValue + surfaces = Set(record.surfaces) actionKind = record.actionKindValue httpMethod = record.httpMethod ?? "POST" urlString = record.urlString ?? "" @@ -72,15 +73,16 @@ struct CustomMenuDraft: Identifiable { var trimmedTitle: String { title.trimmingCharacters(in: .whitespacesAndNewlines) } var isValid: Bool { - guard !trimmedTitle.isEmpty else { return false } + guard !trimmedTitle.isEmpty, !surfaces.isEmpty else { return false } switch actionKind { case .callAPI: return !urlString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty case .createThread: return !messageTemplate.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty case .continueThread: guard !messageTemplate.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return false } - // A thread-menu item continues the tapped thread, so no explicit target - // is needed; project/briefing items must name the thread to continue. - if surface == .thread { return true } + // A thread-only menu item continues the tapped thread, so no explicit + // target is needed; if it also appears on a project/briefing surface it + // must name the thread to continue. + if surfaces == [.thread] { return true } return !targetSessionId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } } @@ -96,7 +98,7 @@ struct CustomMenuDraft: Identifiable { title: trimmedTitle, systemImage: systemImage.isEmpty ? nil : systemImage, projectId: projectId, - surface: surface, + surfaces: CustomMenuItemRecord.Surface.allCases.filter { surfaces.contains($0) }, actionKind: actionKind, httpMethod: actionKind == .callAPI ? httpMethod : nil, urlString: actionKind == .callAPI ? urlString.trimmingCharacters(in: .whitespacesAndNewlines) : nil, @@ -115,6 +117,7 @@ struct CustomMenuEditorSheet: View { @Environment(\.dismiss) private var dismiss @State var draft: CustomMenuDraft + @State private var bodyJSONFormatError: String? let projects: [Project] var onSave: (CustomMenuDraft) -> Void @@ -141,7 +144,17 @@ struct CustomMenuEditorSheet: View { footer } - .frame(width: 540, height: 600) + .frame(width: 540, height: 700) + } + + /// Toggles membership of `surface` in the draft's surface set. + private func surfaceBinding(_ surface: CustomMenuItemRecord.Surface) -> Binding { + Binding( + get: { draft.surfaces.contains(surface) }, + set: { isOn in + if isOn { draft.surfaces.insert(surface) } else { draft.surfaces.remove(surface) } + } + ) } // MARK: - Sections @@ -153,11 +166,15 @@ struct CustomMenuEditorSheet: View { SFSymbolPicker(symbol: $draft.systemImage) } .help("An SF Symbol shown beside the title, e.g. \"bolt\".") - Picker("Show in", selection: $draft.surface) { - Text("Project menu").tag(CustomMenuItemRecord.Surface.project) - Text("Thread menu").tag(CustomMenuItemRecord.Surface.thread) - Text("Briefing card menu").tag(CustomMenuItemRecord.Surface.briefing) + LabeledContent("Show in") { + VStack(alignment: .leading, spacing: 4) { + Toggle("Project menu", isOn: surfaceBinding(.project)) + Toggle("Thread menu", isOn: surfaceBinding(.thread)) + Toggle("Briefing card menu", isOn: surfaceBinding(.briefing)) + } + .toggleStyle(.checkbox) } + .help("Pick one or more menus this item appears on.") Picker("Scope", selection: $draft.projectId) { Text("All projects").tag(UUID?.none) ForEach(projects) { project in @@ -190,13 +207,33 @@ struct CustomMenuEditorSheet: View { headersEditor VStack(alignment: .leading, spacing: 4) { - Text("Body") - .font(.system(size: ClaudeTheme.size(11))) - .foregroundStyle(.secondary) - TextEditor(text: $draft.bodyTemplate) - .font(.system(size: ClaudeTheme.size(12), design: .monospaced)) - .frame(minHeight: 90) + HStack(alignment: .firstTextBaseline) { + Text("Body (JSON)") + .font(.system(size: ClaudeTheme.size(11))) + .foregroundStyle(.secondary) + Spacer() + Button { + prettifyBodyJSON() + } label: { + Label("Prettify", systemImage: "wand.and.stars") + } + .buttonStyle(.bordered) + .controlSize(.small) + .disabled(draft.bodyTemplate.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + .help("Format the JSON body.") + } + JSONCodeEditor(text: $draft.bodyTemplate, fontSize: ClaudeTheme.size(12), minHeight: 200) + .frame(minHeight: 200) + .clipShape(RoundedRectangle(cornerRadius: 6)) .overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(Color(NSColor.separatorColor))) + .onChange(of: draft.bodyTemplate) { _, _ in + bodyJSONFormatError = nil + } + if let bodyJSONFormatError { + Text(bodyJSONFormatError) + .font(.system(size: ClaudeTheme.size(11))) + .foregroundStyle(ClaudeTheme.statusError) + } } } } @@ -268,4 +305,22 @@ struct CustomMenuEditorSheet: View { .padding(.horizontal, 20) .padding(.vertical, 14) } + + private func prettifyBodyJSON() { + let trimmed = draft.bodyTemplate.trimmingCharacters(in: .whitespacesAndNewlines) + guard let data = trimmed.data(using: .utf8), + let object = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]), + let formatted = try? JSONSerialization.data( + withJSONObject: object, + options: [.prettyPrinted, .withoutEscapingSlashes, .fragmentsAllowed] + ), + let string = String(data: formatted, encoding: .utf8) + else { + bodyJSONFormatError = String(localized: "Body must be valid JSON before it can be formatted.") + return + } + + draft.bodyTemplate = string + bodyJSONFormatError = nil + } } diff --git a/RxCode/Views/Settings/CustomMenusSettingsTab.swift b/RxCode/Views/Settings/CustomMenusSettingsTab.swift index b927bed8..ec2db191 100644 --- a/RxCode/Views/Settings/CustomMenusSettingsTab.swift +++ b/RxCode/Views/Settings/CustomMenusSettingsTab.swift @@ -147,7 +147,7 @@ private struct CustomMenuRow: View { Text(verbatim: record.title) .font(.system(size: ClaudeTheme.size(13), weight: .medium)) HStack(spacing: 6) { - badge(surfaceLabel) + ForEach(record.surfaces, id: \.self) { badge(surfaceLabel($0)) } badge(actionLabel) Text(verbatim: projectName) .font(.system(size: ClaudeTheme.size(10))) @@ -192,8 +192,8 @@ private struct CustomMenuRow: View { .clipShape(RoundedRectangle(cornerRadius: 4)) } - private var surfaceLabel: String { - switch record.surfaceValue { + private func surfaceLabel(_ surface: CustomMenuItemRecord.Surface) -> String { + switch surface { case .project: return "Project" case .thread: return "Thread" case .briefing: return "Briefing" diff --git a/RxCode/Views/Settings/JSONCodeEditor.swift b/RxCode/Views/Settings/JSONCodeEditor.swift new file mode 100644 index 00000000..92c74fe8 --- /dev/null +++ b/RxCode/Views/Settings/JSONCodeEditor.swift @@ -0,0 +1,30 @@ +#if os(macOS) +import SwiftUI +import RxCodeEditor + +/// A lightweight JSON editor with live syntax highlighting, used by the custom +/// menu editor for the request body. +struct JSONCodeEditor: View { + @Binding var text: String + var fontSize: CGFloat = 12 + var minHeight: CGFloat = 200 + + private let placeholderProvider = PredefinedAutocompleteProvider.placeholders([ + "projectName", + "projectPath", + "gitHubRepo", + "branch", + "sessionId", + ]) + + var body: some View { + CodeEditorView( + text: $text, + language: "json", + fontSize: fontSize, + autocompleteProvider: placeholderProvider + ) + .frame(minHeight: minHeight) + } +} +#endif diff --git a/RxCode/Views/Sidebar/FileInspectorView.swift b/RxCode/Views/Sidebar/FileInspectorView.swift index 6a79db0e..97cd9211 100644 --- a/RxCode/Views/Sidebar/FileInspectorView.swift +++ b/RxCode/Views/Sidebar/FileInspectorView.swift @@ -248,10 +248,7 @@ struct FileInspectorView: View { }.value content = textToSave let ext = fileExtension - let highlighted = await Task.detached { - SyntaxHighlighter.highlightNS(textToSave, language: ext) - }.value - highlightedContent = highlighted + highlightedContent = await SyntaxHighlighter.highlightNSAsync(textToSave, language: ext) isEditing = false } catch { saveError = "Save failed: \(error.localizedDescription)" @@ -280,11 +277,8 @@ struct FileInspectorView: View { if let text = String(data: data, encoding: .utf8) { let ext = fileExtension - let highlighted = await Task.detached { - SyntaxHighlighter.highlightNS(text, language: ext) - }.value content = text - highlightedContent = highlighted + highlightedContent = await SyntaxHighlighter.highlightNSAsync(text, language: ext) lineCount = text.components(separatedBy: "\n").count } else { errorMessage = "Binary file — preview not available" diff --git a/RxCodeAndroid/app/build.gradle.kts b/RxCodeAndroid/app/build.gradle.kts index 05afc458..0813d75b 100644 --- a/RxCodeAndroid/app/build.gradle.kts +++ b/RxCodeAndroid/app/build.gradle.kts @@ -65,6 +65,8 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 + // Required by the android-tree-sitter AARs. + isCoreLibraryDesugaringEnabled = true } kotlinOptions { jvmTarget = "17" @@ -126,6 +128,17 @@ dependencies { implementation(libs.coil.compose) implementation(libs.coil.svg) implementation(libs.compose.markdown) + + // Tree-sitter syntax highlighting (core + bundled grammars). + coreLibraryDesugaring(libs.desugar.jdk.libs) + implementation(libs.treesitter.core) + implementation(libs.treesitter.java) + implementation(libs.treesitter.kotlin) + implementation(libs.treesitter.python) + implementation(libs.treesitter.json) + implementation(libs.treesitter.c) + implementation(libs.treesitter.cpp) + implementation(libs.treesitter.xml) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.messaging) implementation(libs.firebase.analytics) diff --git a/RxCodeAndroid/app/src/main/assets/queries/c/highlights.scm b/RxCodeAndroid/app/src/main/assets/queries/c/highlights.scm new file mode 100644 index 00000000..8ee11890 --- /dev/null +++ b/RxCodeAndroid/app/src/main/assets/queries/c/highlights.scm @@ -0,0 +1,81 @@ +(identifier) @variable + +((identifier) @constant + (#match? @constant "^[A-Z][A-Z\\d_]*$")) + +"break" @keyword +"case" @keyword +"const" @keyword +"continue" @keyword +"default" @keyword +"do" @keyword +"else" @keyword +"enum" @keyword +"extern" @keyword +"for" @keyword +"if" @keyword +"inline" @keyword +"return" @keyword +"sizeof" @keyword +"static" @keyword +"struct" @keyword +"switch" @keyword +"typedef" @keyword +"union" @keyword +"volatile" @keyword +"while" @keyword + +"#define" @keyword +"#elif" @keyword +"#else" @keyword +"#endif" @keyword +"#if" @keyword +"#ifdef" @keyword +"#ifndef" @keyword +"#include" @keyword +(preproc_directive) @keyword + +"--" @operator +"-" @operator +"-=" @operator +"->" @operator +"=" @operator +"!=" @operator +"*" @operator +"&" @operator +"&&" @operator +"+" @operator +"++" @operator +"+=" @operator +"<" @operator +"==" @operator +">" @operator +"||" @operator + +"." @delimiter +";" @delimiter + +(string_literal) @string +(system_lib_string) @string + +(null) @constant +(number_literal) @number +(char_literal) @number + +(field_identifier) @property +(statement_identifier) @label +(type_identifier) @type +(primitive_type) @type +(sized_type_specifier) @type + +(call_expression + function: (identifier) @function) +(call_expression + function: (field_expression + field: (field_identifier) @function)) +(function_declarator + declarator: (identifier) @function) +(preproc_function_def + name: (identifier) @function.special) + +(comment) @comment diff --git a/RxCodeAndroid/app/src/main/assets/queries/cpp/highlights.scm b/RxCodeAndroid/app/src/main/assets/queries/cpp/highlights.scm new file mode 100644 index 00000000..394d4f9e --- /dev/null +++ b/RxCodeAndroid/app/src/main/assets/queries/cpp/highlights.scm @@ -0,0 +1,70 @@ +; Functions + +(call_expression + function: (qualified_identifier + name: (identifier) @function)) + +(template_function + name: (identifier) @function) + +(template_method + name: (field_identifier) @function) + +(template_function + name: (identifier) @function) + +(function_declarator + declarator: (qualified_identifier + name: (identifier) @function)) + +(function_declarator + declarator: (field_identifier) @function) + +; Types + +((namespace_identifier) @type + (#match? @type "^[A-Z]")) + +(auto) @type + +; Constants + +(this) @variable.builtin +(null "nullptr" @constant) + +; Keywords + +[ + "catch" + "class" + "co_await" + "co_return" + "co_yield" + "constexpr" + "constinit" + "consteval" + "delete" + "explicit" + "final" + "friend" + "mutable" + "namespace" + "noexcept" + "new" + "override" + "private" + "protected" + "public" + "template" + "throw" + "try" + "typename" + "using" + "concept" + "requires" + "virtual" +] @keyword + +; Strings + +(raw_string_literal) @string diff --git a/RxCodeAndroid/app/src/main/assets/queries/java/highlights.scm b/RxCodeAndroid/app/src/main/assets/queries/java/highlights.scm new file mode 100644 index 00000000..b13b4f46 --- /dev/null +++ b/RxCodeAndroid/app/src/main/assets/queries/java/highlights.scm @@ -0,0 +1,149 @@ +; Variables + +(identifier) @variable + +; Methods + +(method_declaration + name: (identifier) @function.method) +(method_invocation + name: (identifier) @function.method) +(super) @function.builtin + +; Annotations + +(annotation + name: (identifier) @attribute) +(marker_annotation + name: (identifier) @attribute) + +"@" @operator + +; Types + +(type_identifier) @type + +(interface_declaration + name: (identifier) @type) +(class_declaration + name: (identifier) @type) +(enum_declaration + name: (identifier) @type) + +((field_access + object: (identifier) @type) + (#match? @type "^[A-Z]")) +((scoped_identifier + scope: (identifier) @type) + (#match? @type "^[A-Z]")) +((method_invocation + object: (identifier) @type) + (#match? @type "^[A-Z]")) +((method_reference + . (identifier) @type) + (#match? @type "^[A-Z]")) + +(constructor_declaration + name: (identifier) @type) + +[ + (boolean_type) + (integral_type) + (floating_point_type) + (floating_point_type) + (void_type) +] @type.builtin + +; Constants + +((identifier) @constant + (#match? @constant "^_*[A-Z][A-Z\\d_]+$")) + +; Builtins + +(this) @variable.builtin + +; Literals + +[ + (hex_integer_literal) + (decimal_integer_literal) + (octal_integer_literal) + (decimal_floating_point_literal) + (hex_floating_point_literal) +] @number + +[ + (character_literal) + (string_literal) +] @string +(escape_sequence) @string.escape + +[ + (true) + (false) + (null_literal) +] @constant.builtin + +[ + (line_comment) + (block_comment) +] @comment + +; Keywords + +[ + "abstract" + "assert" + "break" + "case" + "catch" + "class" + "continue" + "default" + "do" + "else" + "enum" + "exports" + "extends" + "final" + "finally" + "for" + "if" + "implements" + "import" + "instanceof" + "interface" + "module" + "native" + "new" + "non-sealed" + "open" + "opens" + "package" + "permits" + "private" + "protected" + "provides" + "public" + "requires" + "record" + "return" + "sealed" + "static" + "strictfp" + "switch" + "synchronized" + "throw" + "throws" + "to" + "transient" + "transitive" + "try" + "uses" + "volatile" + "when" + "while" + "with" + "yield" +] @keyword diff --git a/RxCodeAndroid/app/src/main/assets/queries/json/highlights.scm b/RxCodeAndroid/app/src/main/assets/queries/json/highlights.scm new file mode 100644 index 00000000..ece8392f --- /dev/null +++ b/RxCodeAndroid/app/src/main/assets/queries/json/highlights.scm @@ -0,0 +1,16 @@ +(pair + key: (_) @string.special.key) + +(string) @string + +(number) @number + +[ + (null) + (true) + (false) +] @constant.builtin + +(escape_sequence) @escape + +(comment) @comment diff --git a/RxCodeAndroid/app/src/main/assets/queries/kotlin/highlights.scm b/RxCodeAndroid/app/src/main/assets/queries/kotlin/highlights.scm new file mode 100644 index 00000000..d2e15a68 --- /dev/null +++ b/RxCodeAndroid/app/src/main/assets/queries/kotlin/highlights.scm @@ -0,0 +1,380 @@ +;; Based on the nvim-treesitter highlighting, which is under the Apache license. +;; See https://github.com/nvim-treesitter/nvim-treesitter/blob/f8ab59861eed4a1c168505e3433462ed800f2bae/queries/kotlin/highlights.scm +;; +;; The only difference in this file is that queries using #lua-match? +;; have been removed. + +;;; Identifiers + +(simple_identifier) @variable + +; `it` keyword inside lambdas +; FIXME: This will highlight the keyword outside of lambdas since tree-sitter +; does not allow us to check for arbitrary nestation +((simple_identifier) @variable.builtin +(#eq? @variable.builtin "it")) + +; `field` keyword inside property getter/setter +; FIXME: This will highlight the keyword outside of getters and setters +; since tree-sitter does not allow us to check for arbitrary nestation +((simple_identifier) @variable.builtin +(#eq? @variable.builtin "field")) + +; `this` this keyword inside classes +(this_expression) @variable.builtin + +; `super` keyword inside classes +(super_expression) @variable.builtin + +(class_parameter + (simple_identifier) @property) + +(class_body + (property_declaration + (variable_declaration + (simple_identifier) @property))) + +; id_1.id_2.id_3: `id_2` and `id_3` are assumed as object properties +(_ + (navigation_suffix + (simple_identifier) @property)) + +(enum_entry + (simple_identifier) @constant) + +(type_identifier) @type + +((type_identifier) @type.builtin + (#any-of? @type.builtin + "Byte" + "Short" + "Int" + "Long" + "UByte" + "UShort" + "UInt" + "ULong" + "Float" + "Double" + "Boolean" + "Char" + "String" + "Array" + "ByteArray" + "ShortArray" + "IntArray" + "LongArray" + "UByteArray" + "UShortArray" + "UIntArray" + "ULongArray" + "FloatArray" + "DoubleArray" + "BooleanArray" + "CharArray" + "Map" + "Set" + "List" + "EmptyMap" + "EmptySet" + "EmptyList" + "MutableMap" + "MutableSet" + "MutableList" +)) + +(package_header + . (identifier)) @namespace + +(import_header + "import" @include) + + +; TODO: Seperate labeled returns/breaks/continue/super/this +; Must be implemented in the parser first +(label) @label + +;;; Function definitions + +(function_declaration + . (simple_identifier) @function) + +(getter + ("get") @function.builtin) +(setter + ("set") @function.builtin) + +(primary_constructor) @constructor +(secondary_constructor + ("constructor") @constructor) + +(constructor_invocation + (user_type + (type_identifier) @constructor)) + +(anonymous_initializer + ("init") @constructor) + +(parameter + (simple_identifier) @parameter) + +(parameter_with_optional_type + (simple_identifier) @parameter) + +; lambda parameters +(lambda_literal + (lambda_parameters + (variable_declaration + (simple_identifier) @parameter))) + +;;; Function calls + +; function() +(call_expression + . (simple_identifier) @function) + +; object.function() or object.property.function() +(call_expression + (navigation_expression + (navigation_suffix + (simple_identifier) @function) . )) + +(call_expression + . (simple_identifier) @function.builtin + (#any-of? @function.builtin + "arrayOf" + "arrayOfNulls" + "byteArrayOf" + "shortArrayOf" + "intArrayOf" + "longArrayOf" + "ubyteArrayOf" + "ushortArrayOf" + "uintArrayOf" + "ulongArrayOf" + "floatArrayOf" + "doubleArrayOf" + "booleanArrayOf" + "charArrayOf" + "emptyArray" + "mapOf" + "setOf" + "listOf" + "emptyMap" + "emptySet" + "emptyList" + "mutableMapOf" + "mutableSetOf" + "mutableListOf" + "print" + "println" + "error" + "TODO" + "run" + "runCatching" + "repeat" + "lazy" + "lazyOf" + "enumValues" + "enumValueOf" + "assert" + "check" + "checkNotNull" + "require" + "requireNotNull" + "with" + "suspend" + "synchronized" +)) + +;;; Literals + +[ + (line_comment) + (multiline_comment) + (shebang_line) +] @comment + +(real_literal) @float +[ + (integer_literal) + (long_literal) + (hex_literal) + (bin_literal) + (unsigned_literal) +] @number + +[ + "null" ; should be highlighted the same as booleans + (boolean_literal) +] @boolean + +(character_literal) @character + +(string_literal) @string + +(character_escape_seq) @string.escape + +; There are 3 ways to define a regex +; - "[abc]?".toRegex() +(call_expression + (navigation_expression + ((string_literal) @string.regex) + (navigation_suffix + ((simple_identifier) @_function + (#eq? @_function "toRegex"))))) + +; - Regex("[abc]?") +(call_expression + ((simple_identifier) @_function + (#eq? @_function "Regex")) + (call_suffix + (value_arguments + (value_argument + (string_literal) @string.regex)))) + +; - Regex.fromLiteral("[abc]?") +(call_expression + (navigation_expression + ((simple_identifier) @_class + (#eq? @_class "Regex")) + (navigation_suffix + ((simple_identifier) @_function + (#eq? @_function "fromLiteral")))) + (call_suffix + (value_arguments + (value_argument + (string_literal) @string.regex)))) + +;;; Keywords + +(type_alias "typealias" @keyword) +[ + (class_modifier) + (member_modifier) + (function_modifier) + (property_modifier) + (platform_modifier) + (variance_modifier) + (parameter_modifier) + (visibility_modifier) + (reification_modifier) + (inheritance_modifier) +]@keyword + +[ + "val" + "var" + "enum" + "class" + "object" + "interface" +; "typeof" ; NOTE: It is reserved for future use +] @keyword + +("fun") @keyword.function + +(jump_expression) @keyword.return + +[ + "if" + "else" + "when" +] @conditional + +[ + "for" + "do" + "while" +] @repeat + +[ + "try" + "catch" + "throw" + "finally" +] @exception + + +(annotation + "@" @attribute (use_site_target)? @attribute) +(annotation + (user_type + (type_identifier) @attribute)) +(annotation + (constructor_invocation + (user_type + (type_identifier) @attribute))) + +(file_annotation + "@" @attribute "file" @attribute ":" @attribute) +(file_annotation + (user_type + (type_identifier) @attribute)) +(file_annotation + (constructor_invocation + (user_type + (type_identifier) @attribute))) + +;;; Operators & Punctuation + +[ + "!" + "!=" + "!==" + "=" + "==" + "===" + ">" + ">=" + "<" + "<=" + "||" + "&&" + "+" + "++" + "+=" + "-" + "--" + "-=" + "*" + "*=" + "/" + "/=" + "%" + "%=" + "?." + "?:" + "!!" + "is" + "!is" + "in" + "!in" + "as" + "as?" + ".." + "->" +] @operator + +[ + "(" ")" + "[" "]" + "{" "}" +] @punctuation.bracket + +[ + "." + "," + ";" + ":" + "::" +] @punctuation.delimiter + +; NOTE: `interpolated_identifier`s can be highlighted in any way +(string_literal + "$" @punctuation.special + (interpolated_identifier) @none) +(string_literal + "${" @punctuation.special + (interpolated_expression) @none + "}" @punctuation.special) diff --git a/RxCodeAndroid/app/src/main/assets/queries/python/highlights.scm b/RxCodeAndroid/app/src/main/assets/queries/python/highlights.scm new file mode 100644 index 00000000..af744484 --- /dev/null +++ b/RxCodeAndroid/app/src/main/assets/queries/python/highlights.scm @@ -0,0 +1,137 @@ +; Identifier naming conventions + +(identifier) @variable + +((identifier) @constructor + (#match? @constructor "^[A-Z]")) + +((identifier) @constant + (#match? @constant "^[A-Z][A-Z_]*$")) + +; Function calls + +(decorator) @function +(decorator + (identifier) @function) + +(call + function: (attribute attribute: (identifier) @function.method)) +(call + function: (identifier) @function) + +; Builtin functions + +((call + function: (identifier) @function.builtin) + (#match? + @function.builtin + "^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$")) + +; Function definitions + +(function_definition + name: (identifier) @function) + +(attribute attribute: (identifier) @property) +(type (identifier) @type) + +; Literals + +[ + (none) + (true) + (false) +] @constant.builtin + +[ + (integer) + (float) +] @number + +(comment) @comment +(string) @string +(escape_sequence) @escape + +(interpolation + "{" @punctuation.special + "}" @punctuation.special) @embedded + +[ + "-" + "-=" + "!=" + "*" + "**" + "**=" + "*=" + "/" + "//" + "//=" + "/=" + "&" + "&=" + "%" + "%=" + "^" + "^=" + "+" + "->" + "+=" + "<" + "<<" + "<<=" + "<=" + "<>" + "=" + ":=" + "==" + ">" + ">=" + ">>" + ">>=" + "|" + "|=" + "~" + "@=" + "and" + "in" + "is" + "not" + "or" + "is not" + "not in" +] @operator + +[ + "as" + "assert" + "async" + "await" + "break" + "class" + "continue" + "def" + "del" + "elif" + "else" + "except" + "exec" + "finally" + "for" + "from" + "global" + "if" + "import" + "lambda" + "nonlocal" + "pass" + "print" + "raise" + "return" + "try" + "while" + "with" + "yield" + "match" + "case" +] @keyword diff --git a/RxCodeAndroid/app/src/main/assets/queries/xml/highlights.scm b/RxCodeAndroid/app/src/main/assets/queries/xml/highlights.scm new file mode 100644 index 00000000..9861eea1 --- /dev/null +++ b/RxCodeAndroid/app/src/main/assets/queries/xml/highlights.scm @@ -0,0 +1,168 @@ +;; XML declaration + +"xml" @keyword + +[ "version" "encoding" "standalone" ] @property + +(EncName) @string.special + +(VersionNum) @number + +[ "yes" "no" ] @boolean + +;; Processing instructions + +(PI) @embedded + +(PI (PITarget) @keyword) + +;; Element declaration + +(elementdecl + "ELEMENT" @keyword + (Name) @tag) + +(contentspec + (_ (Name) @property)) + +"#PCDATA" @type.builtin + +[ "EMPTY" "ANY" ] @string.special.symbol + +[ "*" "?" "+" ] @operator + +;; Entity declaration + +(GEDecl + "ENTITY" @keyword + (Name) @constant) + +(GEDecl (EntityValue) @string) + +(NDataDecl + "NDATA" @keyword + (Name) @label) + +;; Parsed entity declaration + +(PEDecl + "ENTITY" @keyword + "%" @operator + (Name) @constant) + +(PEDecl (EntityValue) @string) + +;; Notation declaration + +(NotationDecl + "NOTATION" @keyword + (Name) @constant) + +(NotationDecl + (ExternalID + (SystemLiteral (URI) @string.special))) + +;; Attlist declaration + +(AttlistDecl + "ATTLIST" @keyword + (Name) @tag) + +(AttDef (Name) @property) + +(AttDef (Enumeration (Nmtoken) @string)) + +(DefaultDecl (AttValue) @string) + +[ + (StringType) + (TokenizedType) +] @type.builtin + +(NotationType "NOTATION" @type.builtin) + +[ + "#REQUIRED" + "#IMPLIED" + "#FIXED" +] @attribute + +;; Entities + +(EntityRef) @constant + +((EntityRef) @constant.builtin + (#any-of? @constant.builtin + "&" "<" ">" """ "'")) + +(CharRef) @constant + +(PEReference) @constant + +;; External references + +[ "PUBLIC" "SYSTEM" ] @keyword + +(PubidLiteral) @string.special + +(SystemLiteral (URI) @markup.link) + +;; Processing instructions + +(XmlModelPI "xml-model" @keyword) + +(StyleSheetPI "xml-stylesheet" @keyword) + +(PseudoAtt (Name) @property) + +(PseudoAtt (PseudoAttValue) @string) + +;; Doctype declaration + +(doctypedecl "DOCTYPE" @keyword) + +(doctypedecl (Name) @type) + +;; Tags + +(STag (Name) @tag) + +(ETag (Name) @tag) + +(EmptyElemTag (Name) @tag) + +;; Attributes + +(Attribute (Name) @property) + +(Attribute (AttValue) @string) + +;; Delimiters & punctuation + +[ + "" + "" + "<" ">" + "" +] @punctuation.delimiter + +[ "(" ")" "[" "]" ] @punctuation.bracket + +[ "\"" "'" ] @punctuation.delimiter + +[ "," "|" "=" ] @operator + +;; Text + +(CharData) @markup + +(CDSect + (CDStart) @markup.heading + (CData) @markup.raw + "]]>" @markup.heading) + +;; Misc + +(Comment) @comment + +(ERROR) @error diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/RxCodeApplication.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/RxCodeApplication.kt index 29e49630..beb48537 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/RxCodeApplication.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/RxCodeApplication.kt @@ -5,6 +5,7 @@ import android.util.Log import coil.ImageLoader import coil.ImageLoaderFactory import coil.decode.SvgDecoder +import app.rxlab.rxcode.ui.util.CodeHighlighter import com.google.firebase.FirebaseApp import com.google.firebase.crashlytics.FirebaseCrashlytics import dagger.hilt.android.HiltAndroidApp @@ -14,6 +15,9 @@ class RxCodeApplication : Application(), ImageLoaderFactory { override fun onCreate() { super.onCreate() Log.w(TAG, "RxCodeApplication launched") + // Load the tree-sitter native library once. If it fails (unexpected ABI, + // missing .so), syntax highlighting silently falls back to plain text. + CodeHighlighter.initialize(this) // FirebaseApp.initializeApp is a no-op when google-services.json is absent // (e.g. local dev without secrets); both calls return null gracefully then. if (FirebaseApp.initializeApp(this) != null) { diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingDetailScreen.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingDetailScreen.kt index 97092fe2..410e0c0a 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingDetailScreen.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingDetailScreen.kt @@ -56,6 +56,7 @@ import app.rxlab.rxcode.state.MobileState import app.rxlab.rxcode.ui.autopilot.ProjectActionsMenu import app.rxlab.rxcode.ui.sheets.NewThreadSheet import app.rxlab.rxcode.ui.util.HapticEvent +import app.rxlab.rxcode.ui.util.MarkdownWithCode import app.rxlab.rxcode.ui.util.RxMarkdownText import app.rxlab.rxcode.ui.util.rememberHaptics import app.rxlab.rxcode.ui.util.relativeTime @@ -346,7 +347,7 @@ private fun SummaryCard(text: String) { ) } if (text.isNotEmpty()) { - RxMarkdownText( + MarkdownWithCode( markdown = text, style = MaterialTheme.typography.bodyMedium, ) diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/chat/ChatScreen.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/chat/ChatScreen.kt index 4e111bbc..cee6a15c 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/chat/ChatScreen.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/chat/ChatScreen.kt @@ -114,6 +114,7 @@ import app.rxlab.rxcode.proto.TodoExtractor import app.rxlab.rxcode.proto.TodoItem import app.rxlab.rxcode.ui.sheets.newBashRunProfile import app.rxlab.rxcode.ui.util.HapticEvent +import app.rxlab.rxcode.ui.util.MarkdownWithCode import app.rxlab.rxcode.ui.util.RxMarkdownText import app.rxlab.rxcode.ui.util.rememberHaptics import kotlinx.coroutines.flow.distinctUntilChanged @@ -620,7 +621,7 @@ private fun MessageBubble( assistantRenderBlocks(msg).forEach { block -> when (block) { is AssistantRenderBlock.Text -> { - RxMarkdownText( + MarkdownWithCode( markdown = block.text, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/util/CodeBlock.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/util/CodeBlock.kt new file mode 100644 index 00000000..daf94eb3 --- /dev/null +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/util/CodeBlock.kt @@ -0,0 +1,201 @@ +package app.rxlab.rxcode.ui.util + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay + +/** A piece of assistant markdown: either prose or a fenced code block. */ +sealed interface MarkdownSegment { + data class Prose(val text: String) : MarkdownSegment + data class Code(val language: String, val content: String) : MarkdownSegment +} + +private val fencedCodeRegex = + Regex("```[ \\t]*([A-Za-z0-9_+#.-]*)[ \\t]*\\r?\\n([\\s\\S]*?)```", RegexOption.MULTILINE) + +/** + * Split markdown into prose and fenced-code segments so code can be rendered with + * syntax highlighting while prose keeps going through the markdown renderer. + * Inline code spans (single backticks) are left inside the prose. + */ +fun splitMarkdownCodeBlocks(markdown: String): List { + val segments = mutableListOf() + var last = 0 + for (match in fencedCodeRegex.findAll(markdown)) { + if (match.range.first > last) { + val prose = markdown.substring(last, match.range.first) + if (prose.isNotBlank()) segments.add(MarkdownSegment.Prose(prose)) + } + val language = match.groupValues[1].trim() + val content = match.groupValues[2].removeSuffix("\n") + segments.add(MarkdownSegment.Code(language, content)) + last = match.range.last + 1 + } + if (last < markdown.length) { + val tail = markdown.substring(last) + if (tail.isNotBlank()) segments.add(MarkdownSegment.Prose(tail)) + } + if (segments.isEmpty()) segments.add(MarkdownSegment.Prose(markdown)) + return segments +} + +/** + * Renders assistant markdown, routing fenced code blocks through the tree-sitter + * [HighlightedCodeBlock] and everything else through [RxMarkdownText]. + */ +@Composable +fun MarkdownWithCode( + markdown: String, + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.onSurface, + style: TextStyle = MaterialTheme.typography.bodyMedium, + linkColor: Color = MaterialTheme.colorScheme.primary, +) { + val segments = remember(markdown) { splitMarkdownCodeBlocks(markdown) } + // Fast path: no fenced code — render exactly as before. + if (segments.size == 1 && segments[0] is MarkdownSegment.Prose) { + RxMarkdownText(markdown = markdown, modifier = modifier, color = color, style = style, linkColor = linkColor) + return + } + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { + for (segment in segments) { + when (segment) { + is MarkdownSegment.Prose -> RxMarkdownText( + markdown = segment.text.trim(), + color = color, + style = style, + linkColor = linkColor, + ) + is MarkdownSegment.Code -> HighlightedCodeBlock( + code = segment.content, + language = segment.language, + ) + } + } + } +} + +/** + * A fenced code block with a language label, copy button, and tree-sitter syntax + * highlighting. Highlighting is computed off the main thread; until it resolves + * (or when the language is unsupported) plain monospaced text is shown. + */ +@Composable +fun HighlightedCodeBlock( + code: String, + language: String, + modifier: Modifier = Modifier, +) { + val dark = isSystemInDarkTheme() + val clipboard = LocalClipboardManager.current + val scheme = MaterialTheme.colorScheme + + var highlighted by remember(code, language, dark) { mutableStateOf(null) } + LaunchedEffect(code, language, dark) { + highlighted = if (CodeHighlighter.supports(language)) { + CodeHighlighter.highlight(code, language, dark) + } else { + null + } + } + + var copied by remember { mutableStateOf(false) } + LaunchedEffect(copied) { + if (copied) { + delay(2000) + copied = false + } + } + + Column( + modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .border(0.5.dp, scheme.outlineVariant, RoundedCornerShape(8.dp)), + ) { + Row( + Modifier + .fillMaxWidth() + .background(scheme.surfaceVariant) + .padding(horizontal = 12.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (language.isNotBlank()) { + Text( + text = language, + style = MaterialTheme.typography.labelSmall, + fontFamily = FontFamily.Monospace, + color = scheme.onSurfaceVariant, + ) + } + Row( + Modifier + .weight(1f) + .padding(start = 8.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = if (copied) Icons.Outlined.Check else Icons.Outlined.ContentCopy, + contentDescription = "Copy code", + tint = if (copied) Color(0xFF4CAF50) else scheme.onSurfaceVariant, + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .clickable { + clipboard.setText(AnnotatedString(code)) + copied = true + } + .padding(2.dp), + ) + } + } + + val horizontalScroll = rememberScrollState() + Text( + text = highlighted ?: AnnotatedString(code), + modifier = Modifier + .fillMaxWidth() + .background(scheme.surface) + .horizontalScroll(horizontalScroll) + .padding(12.dp), + fontFamily = FontFamily.Monospace, + fontSize = 13.sp, + color = scheme.onSurface, + softWrap = false, + textAlign = TextAlign.Start, + ) + } +} diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/util/CodeHighlighter.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/util/CodeHighlighter.kt new file mode 100644 index 00000000..593338db --- /dev/null +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/util/CodeHighlighter.kt @@ -0,0 +1,233 @@ +package app.rxlab.rxcode.ui.util + +import android.content.Context +import android.util.Log +import android.util.LruCache +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import com.itsaky.androidide.treesitter.TSLanguage +import com.itsaky.androidide.treesitter.TSParser +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryCursor +import com.itsaky.androidide.treesitter.TreeSitter +import com.itsaky.androidide.treesitter.c.TSLanguageC +import com.itsaky.androidide.treesitter.cpp.TSLanguageCpp +import com.itsaky.androidide.treesitter.java.TSLanguageJava +import com.itsaky.androidide.treesitter.json.TSLanguageJson +import com.itsaky.androidide.treesitter.kotlin.TSLanguageKotlin +import com.itsaky.androidide.treesitter.python.TSLanguagePython +import com.itsaky.androidide.treesitter.xml.TSLanguageXml +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap + +/** + * Tree-sitter backed syntax highlighter for Android. + * + * Parses a code string with the matching grammar and applies its + * `highlights.scm` capture colors to produce a Compose [AnnotatedString]. + * Grammars ship as native AARs (android-tree-sitter); the highlight queries are + * shipped as app assets under `assets/queries//highlights.scm`. + * + * Only the languages with a bundled grammar are highlighted — Kotlin, Java, + * Python, C, C++, JSON, and XML. Everything else (and any failure) degrades to + * plain text. All native work runs off the main thread and behind a lock, and + * results are cached. + */ +object CodeHighlighter { + private const val TAG = "CodeHighlighter" + + @Volatile private var available = false + private lateinit var appContext: Context + + private class Grammar(val makeLanguage: () -> TSLanguage, val queryAsset: String) + + private val grammars: Map = mapOf( + "kotlin" to Grammar({ TSLanguageKotlin.getInstance() }, "queries/kotlin/highlights.scm"), + "java" to Grammar({ TSLanguageJava.getInstance() }, "queries/java/highlights.scm"), + "python" to Grammar({ TSLanguagePython.getInstance() }, "queries/python/highlights.scm"), + "json" to Grammar({ TSLanguageJson.getInstance() }, "queries/json/highlights.scm"), + "c" to Grammar({ TSLanguageC.getInstance() }, "queries/c/highlights.scm"), + "cpp" to Grammar({ TSLanguageCpp.getInstance() }, "queries/cpp/highlights.scm"), + "xml" to Grammar({ TSLanguageXml.getInstance() }, "queries/xml/highlights.scm"), + ) + + // Native objects are not safe to touch concurrently; serialize all parsing. + private val nativeLock = Any() + private val compiledQueries = ConcurrentHashMap() + private val failedLanguages = ConcurrentHashMap.newKeySet() + private val resultCache = object : LruCache(128) {} + + /** Loads the tree-sitter native library. Safe to call once at startup. */ + fun initialize(context: Context) { + appContext = context.applicationContext + available = try { + TreeSitter.loadLibrary() + true + } catch (t: Throwable) { + Log.w(TAG, "tree-sitter native library failed to load; highlighting disabled", t) + false + } + } + + /** Canonical grammar key for a markdown/file language identifier. */ + fun normalize(language: String): String = when (language.lowercase().trim()) { + "kt", "kts", "kotlin" -> "kotlin" + "java" -> "java" + "py", "python", "py3" -> "python" + "json", "jsonc", "json5", "geojson" -> "json" + "c", "h" -> "c" + "cpp", "cc", "cxx", "c++", "hpp", "hh", "hxx" -> "cpp" + "xml", "xsd", "xsl", "svg", "plist" -> "xml" + else -> language.lowercase().trim() + } + + /** True when a grammar is available to highlight [language]. */ + fun supports(language: String): Boolean = + available && grammars.containsKey(normalize(language)) + + /** + * Highlight [code] for [language], producing a colored [AnnotatedString]. + * Returns plain text when the language is unsupported or parsing fails. + */ + suspend fun highlight(code: String, language: String, dark: Boolean): AnnotatedString = + withContext(Dispatchers.Default) { + val key = normalize(language) + val grammar = grammars[key] + if (!available || grammar == null || code.isEmpty()) { + return@withContext AnnotatedString(code) + } + val cacheKey = "$key|$dark|$code" + resultCache.get(cacheKey)?.let { return@withContext it } + + val result = try { + buildHighlighted(code, key, grammar, dark) + } catch (t: Throwable) { + Log.w(TAG, "highlighting failed for $key; falling back to plain text", t) + AnnotatedString(code) + } + resultCache.put(cacheKey, result) + result + } + + private fun buildHighlighted(code: String, key: String, grammar: Grammar, dark: Boolean): AnnotatedString { + val spans = ArrayList() + synchronized(nativeLock) { + val query = compiledQuery(key, grammar) ?: return AnnotatedString(code) + val parser = TSParser.create() + try { + parser.setLanguage(grammar.makeLanguage()) + val tree = parser.parseString(code) ?: return AnnotatedString(code) + try { + val cursor = TSQueryCursor.create() + try { + cursor.exec(query, tree.rootNode) + var match = cursor.nextMatch() + while (match != null) { + for (capture in match.captures) { + val name = query.getCaptureNameForId(capture.index) + val node = capture.node + spans.add(Span(node.startByte, node.endByte, name)) + } + match = cursor.nextMatch() + } + } finally { + cursor.close() + } + } finally { + tree.close() + } + } finally { + parser.close() + } + } + + val length = code.length + return buildAnnotatedString { + append(code) + for (span in spans) { + // android-tree-sitter offsets are UTF-16 byte offsets; the Kotlin + // String is UTF-16, so divide by two to get char indices. + val start = span.startByte / 2 + val end = span.endByte / 2 + if (start in 0..length && end in start..length && start < end) { + styleFor(span.capture, dark)?.let { addStyle(it, start, end) } + } + } + } + } + + private fun compiledQuery(key: String, grammar: Grammar): TSQuery? { + compiledQueries[key]?.let { return it } + if (failedLanguages.contains(key)) return null + return try { + val scm = appContext.assets.open(grammar.queryAsset).bufferedReader().use { it.readText() } + val query = TSQuery.create(grammar.makeLanguage(), scm) + compiledQueries[key] = query + query + } catch (t: Throwable) { + // A query authored for a newer grammar can reference unknown node + // types; remember the failure and fall back to plain text. + Log.w(TAG, "failed to compile highlights query for $key", t) + failedLanguages.add(key) + null + } + } + + // MARK: - Capture → style mapping + + private class Span(val startByte: Int, val endByte: Int, val capture: String) + + private fun styleFor(capture: String, dark: Boolean): SpanStyle? { + var key = capture + while (true) { + palette[key]?.let { entry -> + return SpanStyle( + color = if (dark) entry.dark else entry.light, + fontWeight = if (entry.bold) FontWeight.Medium else null, + ) + } + val dot = key.lastIndexOf('.') + if (dot < 0) return null + key = key.substring(0, dot) + } + } + + private class Style(val light: Color, val dark: Color, val bold: Boolean = false) + + private val palette: Map = mapOf( + "keyword" to Style(Color(0xFFAF3A93), Color(0xFFFF7AB2), bold = true), + "conditional" to Style(Color(0xFFAF3A93), Color(0xFFFF7AB2), bold = true), + "repeat" to Style(Color(0xFFAF3A93), Color(0xFFFF7AB2), bold = true), + "include" to Style(Color(0xFFAF3A93), Color(0xFFFF7AB2), bold = true), + "exception" to Style(Color(0xFFAF3A93), Color(0xFFFF7AB2), bold = true), + "boolean" to Style(Color(0xFFAF3A93), Color(0xFFFF7AB2)), + "operator" to Style(Color(0xFF3C3929), Color(0xFFCCC9C0)), + "string" to Style(Color(0xFFC4442D), Color(0xFFFF8170)), + "character" to Style(Color(0xFFC4442D), Color(0xFFFF8170)), + "comment" to Style(Color(0xFF72962A), Color(0xFF7EC856)), + "number" to Style(Color(0xFF1C00CF), Color(0xFFD0BF69)), + "float" to Style(Color(0xFF1C00CF), Color(0xFFD0BF69)), + "constant" to Style(Color(0xFF1C00CF), Color(0xFFD0BF69)), + "type" to Style(Color(0xFF5B2699), Color(0xFFDABAFF), bold = true), + "constructor" to Style(Color(0xFF5B2699), Color(0xFFDABAFF), bold = true), + "namespace" to Style(Color(0xFF5B2699), Color(0xFFDABAFF)), + "module" to Style(Color(0xFF5B2699), Color(0xFFDABAFF)), + "function" to Style(Color(0xFF326D74), Color(0xFF78C2B3)), + "method" to Style(Color(0xFF326D74), Color(0xFF78C2B3)), + "property" to Style(Color(0xFF3E6D74), Color(0xFF78C2B3)), + "field" to Style(Color(0xFF3E6D74), Color(0xFF78C2B3)), + "attribute" to Style(Color(0xFF947100), Color(0xFFFFA14F)), + "annotation" to Style(Color(0xFF947100), Color(0xFFFFA14F)), + "decorator" to Style(Color(0xFF947100), Color(0xFFFFA14F)), + "label" to Style(Color(0xFF947100), Color(0xFFFFA14F)), + "escape" to Style(Color(0xFF947100), Color(0xFFFFA14F)), + "tag" to Style(Color(0xFFAF3A93), Color(0xFFFF7AB2), bold = true), + "variable" to Style(Color(0xFF3C3929), Color(0xFFCCC9C0)), + "parameter" to Style(Color(0xFF3C3929), Color(0xFFCCC9C0)), + "punctuation" to Style(Color(0xFF3C3929), Color(0xFFCCC9C0)), + ) +} diff --git a/RxCodeAndroid/gradle/libs.versions.toml b/RxCodeAndroid/gradle/libs.versions.toml index ac7a5bb2..385eef86 100644 --- a/RxCodeAndroid/gradle/libs.versions.toml +++ b/RxCodeAndroid/gradle/libs.versions.toml @@ -22,6 +22,8 @@ mlkitBarcode = "17.3.0" accompanistPermissions = "0.34.0" coil = "2.7.0" composeMarkdown = "0.5.4" +treesitter = "4.3.2" +desugarJdkLibs = "2.1.4" androidxWebkit = "1.12.1" firebaseBom = "33.5.1" googleServices = "4.4.2" @@ -79,6 +81,19 @@ accompanist-permissions = { group = "com.google.accompanist", name = "accompanis coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } coil-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" } compose-markdown = { group = "com.github.jeziellago", name = "compose-markdown", version.ref = "composeMarkdown" } + +# Tree-sitter syntax highlighting. android-tree-sitter ships a real Android AAR +# (all four ABIs); highlights.scm queries are shipped as app assets since the +# grammar artifacts do not bundle them. +treesitter-core = { group = "com.itsaky.androidide.treesitter", name = "android-tree-sitter", version.ref = "treesitter" } +treesitter-java = { group = "com.itsaky.androidide.treesitter", name = "tree-sitter-java", version.ref = "treesitter" } +treesitter-kotlin = { group = "com.itsaky.androidide.treesitter", name = "tree-sitter-kotlin", version.ref = "treesitter" } +treesitter-python = { group = "com.itsaky.androidide.treesitter", name = "tree-sitter-python", version.ref = "treesitter" } +treesitter-json = { group = "com.itsaky.androidide.treesitter", name = "tree-sitter-json", version.ref = "treesitter" } +treesitter-c = { group = "com.itsaky.androidide.treesitter", name = "tree-sitter-c", version.ref = "treesitter" } +treesitter-cpp = { group = "com.itsaky.androidide.treesitter", name = "tree-sitter-cpp", version.ref = "treesitter" } +treesitter-xml = { group = "com.itsaky.androidide.treesitter", name = "tree-sitter-xml", version.ref = "treesitter" } +desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugarJdkLibs" } firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } From df4f0135c1daed4b094447f3468bf134eb75bf06 Mon Sep 17 00:00:00 2001 From: sirily11 <32106111+sirily11@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:54:46 +0800 Subject: [PATCH 4/4] fix: remove unused tree-sitter package dependencies --- Packages/Package.resolved | 227 +------- Packages/Package.swift | 74 +-- .../TreeSitterScanners/csrc/css_scanner.c | 100 ---- .../csrc/javascript_scanner.c | 364 ------------ .../TreeSitterScanners/csrc/lua_scanner.c | 195 ------- .../TreeSitterScanners/csrc/python_scanner.c | 437 -------------- .../csrc/tree_sitter/alloc.h | 54 -- .../csrc/tree_sitter/array.h | 291 ---------- .../csrc/tree_sitter/parser.h | 286 --------- .../include/treesitter_scanners.h | 2 - .../xcshareddata/swiftpm/Package.resolved | 543 +++++------------- 11 files changed, 161 insertions(+), 2412 deletions(-) delete mode 100644 Packages/Sources/TreeSitterScanners/csrc/css_scanner.c delete mode 100644 Packages/Sources/TreeSitterScanners/csrc/javascript_scanner.c delete mode 100644 Packages/Sources/TreeSitterScanners/csrc/lua_scanner.c delete mode 100644 Packages/Sources/TreeSitterScanners/csrc/python_scanner.c delete mode 100644 Packages/Sources/TreeSitterScanners/csrc/tree_sitter/alloc.h delete mode 100644 Packages/Sources/TreeSitterScanners/csrc/tree_sitter/array.h delete mode 100644 Packages/Sources/TreeSitterScanners/csrc/tree_sitter/parser.h delete mode 100644 Packages/Sources/TreeSitterScanners/include/treesitter_scanners.h diff --git a/Packages/Package.resolved b/Packages/Package.resolved index 2f8dab8f..65e1bf43 100644 --- a/Packages/Package.resolved +++ b/Packages/Package.resolved @@ -1,231 +1,6 @@ { - "originHash" : "b2291e8317a0a55453eb864270a1cacdf570f9cfaed9a3389b87e7e509694a46", + "originHash" : "b101d76c29aab052484573baab03434736d3a84ba21abcc8daa6c7fb36cf1473", "pins" : [ - { - "identity" : "swift-tree-sitter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/swift-tree-sitter", - "state" : { - "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", - "version" : "0.25.0" - } - }, - { - "identity" : "tree-sitter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter", - "state" : { - "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406", - "version" : "0.25.10" - } - }, - { - "identity" : "tree-sitter-bash", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-bash", - "state" : { - "revision" : "a06c2e4415e9bc0346c6b86d401879ffb44058f7", - "version" : "0.25.1" - } - }, - { - "identity" : "tree-sitter-c", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-c", - "state" : { - "revision" : "b780e47fc780ddc8da13afa35a3f4ed5c157823d", - "version" : "0.24.2" - } - }, - { - "identity" : "tree-sitter-c-sharp", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-c-sharp", - "state" : { - "revision" : "cac6d5fb595f5811a076336682d5d595ac1c9e85", - "version" : "0.23.5" - } - }, - { - "identity" : "tree-sitter-cpp", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-cpp", - "state" : { - "revision" : "f41e1a044c8a84ea9fa8577fdd2eab92ec96de02", - "version" : "0.23.4" - } - }, - { - "identity" : "tree-sitter-css", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-css", - "state" : { - "revision" : "dda5cfc5722c429eaba1c910ca32c2c0c5bb1a3f", - "version" : "0.25.0" - } - }, - { - "identity" : "tree-sitter-go", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-go", - "state" : { - "revision" : "1547678a9da59885853f5f5cc8a99cc203fa2e2c", - "version" : "0.25.0" - } - }, - { - "identity" : "tree-sitter-haskell", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-haskell", - "state" : { - "revision" : "c30d812bc90827f1a54106a25bc9a6307f5cdcec", - "version" : "0.23.1" - } - }, - { - "identity" : "tree-sitter-html", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-html", - "state" : { - "revision" : "5a5ca8551a179998360b4a4ca2c0f366a35acc03", - "version" : "0.23.2" - } - }, - { - "identity" : "tree-sitter-java", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-java", - "state" : { - "revision" : "94703d5a6bed02b98e438d7cad1136c01a60ba2c", - "version" : "0.23.5" - } - }, - { - "identity" : "tree-sitter-javascript", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-javascript", - "state" : { - "revision" : "44c892e0be055ac465d5eeddae6d3e194424e7de", - "version" : "0.25.0" - } - }, - { - "identity" : "tree-sitter-json", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-json", - "state" : { - "revision" : "ee35a6ebefcef0c5c416c0d1ccec7370cfca5a24", - "version" : "0.24.8" - } - }, - { - "identity" : "tree-sitter-kotlin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/fwcd/tree-sitter-kotlin", - "state" : { - "revision" : "e1a2d5ad1f61f5740677183cd4125bb071cd2f30", - "version" : "0.3.8" - } - }, - { - "identity" : "tree-sitter-lua", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter-grammars/tree-sitter-lua", - "state" : { - "revision" : "10fe0054734eec83049514ea2e718b2a56acd0c9", - "version" : "0.5.0" - } - }, - { - "identity" : "tree-sitter-markdown", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter-grammars/tree-sitter-markdown", - "state" : { - "revision" : "f969cd3ae3f9fbd4e43205431d0ae286014c05b5", - "version" : "0.5.3" - } - }, - { - "identity" : "tree-sitter-php", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-php", - "state" : { - "revision" : "5b5627faaa290d89eb3d01b9bf47c3bb9e797dea", - "version" : "0.24.2" - } - }, - { - "identity" : "tree-sitter-python", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-python", - "state" : { - "revision" : "293fdc02038ee2bf0e2e206711b69c90ac0d413f", - "version" : "0.25.0" - } - }, - { - "identity" : "tree-sitter-ruby", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-ruby", - "state" : { - "revision" : "71bd32fb7607035768799732addba884a37a6210", - "version" : "0.23.1" - } - }, - { - "identity" : "tree-sitter-rust", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-rust", - "state" : { - "revision" : "77a3747266f4d621d0757825e6b11edcbf991ca5", - "version" : "0.24.2" - } - }, - { - "identity" : "tree-sitter-scala", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-scala", - "state" : { - "revision" : "38950b525c9dfc44c8b60d44bdd6e54217286ca8", - "version" : "0.26.0" - } - }, - { - "identity" : "tree-sitter-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/alex-pinkus/tree-sitter-swift", - "state" : { - "revision" : "31d17fe7e818a2048c808b5c6fdc2dc792f4f5b5", - "version" : "0.7.3-with-generated-files" - } - }, - { - "identity" : "tree-sitter-toml", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter-grammars/tree-sitter-toml", - "state" : { - "revision" : "64b56832c2cffe41758f28e05c756a3a98d16f41", - "version" : "0.7.0" - } - }, - { - "identity" : "tree-sitter-typescript", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-typescript", - "state" : { - "revision" : "f975a621f4e7f532fe322e13c4f79495e0a7b2e7", - "version" : "0.23.2" - } - }, - { - "identity" : "tree-sitter-yaml", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter-grammars/tree-sitter-yaml", - "state" : { - "revision" : "b733d3f5f5005890f324333dd57e1f0badec5c87", - "version" : "0.7.0" - } - }, { "identity" : "viewinspector", "kind" : "remoteSourceControl", diff --git a/Packages/Package.swift b/Packages/Package.swift index 9ea38a92..4c960bcc 100644 --- a/Packages/Package.swift +++ b/Packages/Package.swift @@ -1,66 +1,6 @@ // swift-tools-version: 6.2 import PackageDescription -// Tree-sitter grammar packages used by `SyntaxHighlighter`. Each one ships a -// `tree_sitter_()` parser plus a `queries/highlights.scm` bundle that -// `LanguageConfiguration(_:name:)` auto-discovers. Their own SwiftTreeSitter -// dependency is test-only, so SwiftPM prunes it and the mix of ChimeHQ- and -// tree-sitter-org-hosted SwiftTreeSitter URLs does not conflict with ours. -let grammarPackages: [Package.Dependency] = [ - .package(url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.25.0"), - .package(url: "https://github.com/alex-pinkus/tree-sitter-swift", exact: "0.7.3-with-generated-files"), - .package(url: "https://github.com/tree-sitter/tree-sitter-javascript", exact: "0.25.0"), - .package(url: "https://github.com/tree-sitter/tree-sitter-typescript", exact: "0.23.2"), - .package(url: "https://github.com/tree-sitter/tree-sitter-python", exact: "0.25.0"), - .package(url: "https://github.com/tree-sitter/tree-sitter-json", exact: "0.24.8"), - .package(url: "https://github.com/tree-sitter/tree-sitter-bash", exact: "0.25.1"), - .package(url: "https://github.com/tree-sitter/tree-sitter-go", exact: "0.25.0"), - .package(url: "https://github.com/tree-sitter/tree-sitter-rust", exact: "0.24.2"), - .package(url: "https://github.com/tree-sitter/tree-sitter-ruby", exact: "0.23.1"), - .package(url: "https://github.com/tree-sitter/tree-sitter-java", exact: "0.23.5"), - .package(url: "https://github.com/tree-sitter/tree-sitter-c", exact: "0.24.2"), - .package(url: "https://github.com/tree-sitter/tree-sitter-cpp", exact: "0.23.4"), - .package(url: "https://github.com/tree-sitter/tree-sitter-c-sharp", exact: "0.23.5"), - .package(url: "https://github.com/tree-sitter/tree-sitter-html", exact: "0.23.2"), - .package(url: "https://github.com/tree-sitter/tree-sitter-css", exact: "0.25.0"), - .package(url: "https://github.com/tree-sitter/tree-sitter-php", exact: "0.24.2"), - .package(url: "https://github.com/tree-sitter/tree-sitter-scala", exact: "0.26.0"), - .package(url: "https://github.com/tree-sitter/tree-sitter-haskell", exact: "0.23.1"), - .package(url: "https://github.com/tree-sitter-grammars/tree-sitter-yaml", exact: "0.7.0"), - .package(url: "https://github.com/tree-sitter-grammars/tree-sitter-toml", exact: "0.7.0"), - .package(url: "https://github.com/tree-sitter-grammars/tree-sitter-lua", exact: "0.5.0"), - .package(url: "https://github.com/tree-sitter-grammars/tree-sitter-markdown", exact: "0.5.3"), - .package(url: "https://github.com/fwcd/tree-sitter-kotlin", exact: "0.3.8"), -] - -// Grammar products linked into RxCodeCore (where `SyntaxHighlighter` lives). -let grammarProducts: [Target.Dependency] = [ - .product(name: "SwiftTreeSitter", package: "swift-tree-sitter"), - .product(name: "TreeSitterSwift", package: "tree-sitter-swift"), - .product(name: "TreeSitterJavaScript", package: "tree-sitter-javascript"), - .product(name: "TreeSitterTypeScript", package: "tree-sitter-typescript"), - .product(name: "TreeSitterPython", package: "tree-sitter-python"), - .product(name: "TreeSitterJSON", package: "tree-sitter-json"), - .product(name: "TreeSitterBash", package: "tree-sitter-bash"), - .product(name: "TreeSitterGo", package: "tree-sitter-go"), - .product(name: "TreeSitterRust", package: "tree-sitter-rust"), - .product(name: "TreeSitterRuby", package: "tree-sitter-ruby"), - .product(name: "TreeSitterJava", package: "tree-sitter-java"), - .product(name: "TreeSitterC", package: "tree-sitter-c"), - .product(name: "TreeSitterCPP", package: "tree-sitter-cpp"), - .product(name: "TreeSitterCSharp", package: "tree-sitter-c-sharp"), - .product(name: "TreeSitterHTML", package: "tree-sitter-html"), - .product(name: "TreeSitterCSS", package: "tree-sitter-css"), - .product(name: "TreeSitterPHP", package: "tree-sitter-php"), - .product(name: "TreeSitterScala", package: "tree-sitter-scala"), - .product(name: "TreeSitterHaskell", package: "tree-sitter-haskell"), - .product(name: "TreeSitterYAML", package: "tree-sitter-yaml"), - .product(name: "TreeSitterTOML", package: "tree-sitter-toml"), - .product(name: "TreeSitterLua", package: "tree-sitter-lua"), - .product(name: "TreeSitterMarkdown", package: "tree-sitter-markdown"), - .product(name: "TreeSitterKotlin", package: "tree-sitter-kotlin"), -] - let package = Package( name: "RxCodePackages", defaultLocalization: "en", @@ -76,7 +16,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/nalexn/ViewInspector", from: "0.10.0"), - ] + grammarPackages, + ], targets: [ .target( name: "MessageList", @@ -87,20 +27,8 @@ let package = Package( ), .target( name: "RxCodeCore", - dependencies: grammarProducts + ["TreeSitterScanners"], path: "Sources/RxCodeCore" ), - // Provides external-scanner symbols (`tree_sitter__external_scanner_*`) - // for grammar packages whose SPM manifest omits `scanner.c` when consumed - // as a dependency (a CWD-relative `fileExists` check). Vendored from the - // matching grammar tags. - .target( - name: "TreeSitterScanners", - path: "Sources/TreeSitterScanners", - sources: ["csrc"], - publicHeadersPath: "include", - cSettings: [.headerSearchPath("csrc")] - ), .target( name: "RxCodeMarkdown", dependencies: ["RxCodeCore"], diff --git a/Packages/Sources/TreeSitterScanners/csrc/css_scanner.c b/Packages/Sources/TreeSitterScanners/csrc/css_scanner.c deleted file mode 100644 index ba7dc652..00000000 --- a/Packages/Sources/TreeSitterScanners/csrc/css_scanner.c +++ /dev/null @@ -1,100 +0,0 @@ -#include "tree_sitter/parser.h" - -#include - -enum TokenType { - DESCENDANT_OP, - PSEUDO_CLASS_SELECTOR_COLON, - ERROR_RECOVERY, -}; - -static inline void advance(TSLexer *lexer) { lexer->advance(lexer, false); } - -static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } - -void *tree_sitter_css_external_scanner_create() { return NULL; } - -void tree_sitter_css_external_scanner_destroy(void *payload) {} - -unsigned tree_sitter_css_external_scanner_serialize(void *payload, char *buffer) { return 0; } - -void tree_sitter_css_external_scanner_deserialize(void *payload, const char *buffer, unsigned length) {} - -bool tree_sitter_css_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { - if (valid_symbols[ERROR_RECOVERY]) { - return false; - } - - if (iswspace(lexer->lookahead) && valid_symbols[DESCENDANT_OP]) { - lexer->result_symbol = DESCENDANT_OP; - - skip(lexer); - while (iswspace(lexer->lookahead)) { - skip(lexer); - } - lexer->mark_end(lexer); - - if (lexer->lookahead == '#' || lexer->lookahead == '.' || lexer->lookahead == '[' || lexer->lookahead == '-' || - lexer->lookahead == '*' || iswalnum(lexer->lookahead)) { - return true; - } - - if (lexer->lookahead == ':') { - advance(lexer); - if (iswspace(lexer->lookahead)) { - return false; - } - for (;;) { - if (lexer->lookahead == ';' || lexer->lookahead == '}' || lexer->eof(lexer)) { - return false; - } - if (lexer->lookahead == '{') { - return true; - } - advance(lexer); - } - } - } - - if (valid_symbols[PSEUDO_CLASS_SELECTOR_COLON]) { - while (iswspace(lexer->lookahead)) { - skip(lexer); - } - if (lexer->lookahead == ':') { - advance(lexer); - if (lexer->lookahead == ':') { - return false; - } - lexer->mark_end(lexer); - lexer->result_symbol = PSEUDO_CLASS_SELECTOR_COLON; - - // We need a `{` to be a pseudo class selector, a `;` indicates a property. - // This does not apply if we're in a comment, however. - bool in_comment = false; - while (lexer->lookahead != ';' && lexer->lookahead != '}' && !lexer->eof(lexer)) { - advance(lexer); - if (lexer->lookahead == '{' && !in_comment) { - return true; - } - if (lexer->lookahead == '/' && !in_comment) { - advance(lexer); - if (lexer->lookahead == '*') { - in_comment = true; - } - } else if (lexer->lookahead == '*' && in_comment) { - advance(lexer); - if (lexer->lookahead == '/') { - in_comment = false; - } - } - } - - // If we're at eof, and we happened to *not* find an opening brace to indicate we have a pseudo class - // selector, we should *still* return one at EOF. This will improve error recovery, and the malformed code - // can be parsed as an erroneous pseudo-class selector, rather than an erroneous property. - return lexer->eof(lexer); - } - } - - return false; -} diff --git a/Packages/Sources/TreeSitterScanners/csrc/javascript_scanner.c b/Packages/Sources/TreeSitterScanners/csrc/javascript_scanner.c deleted file mode 100644 index 795916dd..00000000 --- a/Packages/Sources/TreeSitterScanners/csrc/javascript_scanner.c +++ /dev/null @@ -1,364 +0,0 @@ -#include "tree_sitter/parser.h" - -#include -#include - -enum TokenType { - AUTOMATIC_SEMICOLON, - TEMPLATE_CHARS, - TERNARY_QMARK, - HTML_COMMENT, - LOGICAL_OR, - ESCAPE_SEQUENCE, - REGEX_PATTERN, - JSX_TEXT, -}; - -void *tree_sitter_javascript_external_scanner_create() { return NULL; } - -void tree_sitter_javascript_external_scanner_destroy(void *p) {} - -unsigned tree_sitter_javascript_external_scanner_serialize(void *payload, char *buffer) { return 0; } - -void tree_sitter_javascript_external_scanner_deserialize(void *p, const char *b, unsigned n) {} - -static inline void advance(TSLexer *lexer) { lexer->advance(lexer, false); } - -static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } - -static bool scan_template_chars(TSLexer *lexer) { - lexer->result_symbol = TEMPLATE_CHARS; - for (bool has_content = false;; has_content = true) { - lexer->mark_end(lexer); - switch (lexer->lookahead) { - case '`': - return has_content; - case '\0': - return false; - case '$': - advance(lexer); - if (lexer->lookahead == '{') { - return has_content; - } - break; - case '\\': - return has_content; - default: - advance(lexer); - } - } -} - -typedef enum { - REJECT, // Semicolon is illegal, ie a syntax error occurred - NO_NEWLINE, // Unclear if semicolon will be legal, continue - ACCEPT, // Semicolon is legal, assuming a comment was encountered -} WhitespaceResult; - -/** - * @param consume If false, only consume enough to check if comment indicates semicolon-legality - */ -static WhitespaceResult scan_whitespace_and_comments(TSLexer *lexer, bool *scanned_comment, bool consume) { - bool saw_block_newline = false; - - for (;;) { - while (iswspace(lexer->lookahead)) { - skip(lexer); - } - - if (lexer->lookahead == '/') { - skip(lexer); - - if (lexer->lookahead == '/') { - skip(lexer); - while (lexer->lookahead != 0 && lexer->lookahead != '\n' && lexer->lookahead != 0x2028 && - lexer->lookahead != 0x2029) { - skip(lexer); - } - *scanned_comment = true; - } else if (lexer->lookahead == '*') { - skip(lexer); - while (lexer->lookahead != 0) { - if (lexer->lookahead == '*') { - skip(lexer); - if (lexer->lookahead == '/') { - skip(lexer); - *scanned_comment = true; - - if (lexer->lookahead != '/' && !consume) { - return saw_block_newline ? ACCEPT : NO_NEWLINE; - } - - break; - } - } else if (lexer->lookahead == '\n' || lexer->lookahead == 0x2028 || lexer->lookahead == 0x2029) { - saw_block_newline = true; - skip(lexer); - } else { - skip(lexer); - } - } - } else { - return REJECT; - } - } else { - return ACCEPT; - } - } -} - -static bool scan_automatic_semicolon(TSLexer *lexer, bool comment_condition, bool *scanned_comment) { - lexer->result_symbol = AUTOMATIC_SEMICOLON; - lexer->mark_end(lexer); - - for (;;) { - if (lexer->lookahead == 0) { - return true; - } - - if (lexer->lookahead == '/') { - WhitespaceResult result = scan_whitespace_and_comments(lexer, scanned_comment, false); - if (result == REJECT) { - return false; - } - - if (result == ACCEPT && comment_condition && lexer->lookahead != ',' && lexer->lookahead != '=') { - return true; - } - } - - if (lexer->lookahead == '}') { - return true; - } - - if (lexer->is_at_included_range_start(lexer)) { - return true; - } - - if (lexer->lookahead == '\n' || lexer->lookahead == 0x2028 || lexer->lookahead == 0x2029) { - break; - } - - if (!iswspace(lexer->lookahead)) { - return false; - } - - skip(lexer); - } - - skip(lexer); - - if (scan_whitespace_and_comments(lexer, scanned_comment, true) == REJECT) { - return false; - } - - switch (lexer->lookahead) { - case '`': - case ',': - case ':': - case ';': - case '*': - case '%': - case '>': - case '<': - case '=': - case '[': - case '(': - case '?': - case '^': - case '|': - case '&': - case '/': - return false; - - // Insert a semicolon before decimals literals but not otherwise. - case '.': - skip(lexer); - return iswdigit(lexer->lookahead); - - // Insert a semicolon before `--` and `++`, but not before binary `+` or `-`. - case '+': - skip(lexer); - return lexer->lookahead == '+'; - case '-': - skip(lexer); - return lexer->lookahead == '-'; - - // Don't insert a semicolon before `!=`, but do insert one before a unary `!`. - case '!': - skip(lexer); - return lexer->lookahead != '='; - - // Don't insert a semicolon before `in` or `instanceof`, but do insert one - // before an identifier. - case 'i': - skip(lexer); - - if (lexer->lookahead != 'n') { - return true; - } - skip(lexer); - - if (!iswalpha(lexer->lookahead)) { - return false; - } - - for (unsigned i = 0; i < 8; i++) { - if (lexer->lookahead != "stanceof"[i]) { - return true; - } - skip(lexer); - } - - if (!iswalpha(lexer->lookahead)) { - return false; - } - break; - - default: - break; - } - - return true; -} - -static bool scan_ternary_qmark(TSLexer *lexer) { - for (;;) { - if (!iswspace(lexer->lookahead)) { - break; - } - skip(lexer); - } - - if (lexer->lookahead == '?') { - advance(lexer); - - if (lexer->lookahead == '?') { - return false; - } - - lexer->mark_end(lexer); - lexer->result_symbol = TERNARY_QMARK; - - if (lexer->lookahead == '.') { - advance(lexer); - if (iswdigit(lexer->lookahead)) { - return true; - } - return false; - } - return true; - } - return false; -} - -static bool scan_html_comment(TSLexer *lexer) { - while (iswspace(lexer->lookahead) || lexer->lookahead == 0x2028 || lexer->lookahead == 0x2029) { - skip(lexer); - } - - const char *comment_start = ""; - - if (lexer->lookahead == '<') { - for (unsigned i = 0; i < 4; i++) { - if (lexer->lookahead != comment_start[i]) { - return false; - } - advance(lexer); - } - } else if (lexer->lookahead == '-') { - for (unsigned i = 0; i < 3; i++) { - if (lexer->lookahead != comment_end[i]) { - return false; - } - advance(lexer); - } - } else { - return false; - } - - while (lexer->lookahead != 0 && lexer->lookahead != '\n' && lexer->lookahead != 0x2028 && - lexer->lookahead != 0x2029) { - advance(lexer); - } - - lexer->result_symbol = HTML_COMMENT; - lexer->mark_end(lexer); - - return true; -} - -static bool scan_jsx_text(TSLexer *lexer) { - // saw_text will be true if we see any non-whitespace content, or any whitespace content that is not a newline and - // does not immediately follow a newline. - bool saw_text = false; - // at_newline will be true if we are currently at a newline, or if we are at whitespace that is not a newline but - // immediately follows a newline. - bool at_newline = false; - - while (lexer->lookahead != 0 && lexer->lookahead != '<' && lexer->lookahead != '>' && lexer->lookahead != '{' && - lexer->lookahead != '}' && lexer->lookahead != '&') { - bool is_wspace = iswspace(lexer->lookahead); - if (lexer->lookahead == '\n') { - at_newline = true; - } else { - // If at_newline is already true, and we see some whitespace, then it must stay true. - // Otherwise, it should be false. - // - // See the table below to determine the logic for computing `saw_text`. - // - // |------------------------------------| - // | at_newline | is_wspace | saw_text | - // |------------|-----------|-----------| - // | false (0) | false (0) | true (1) | - // | false (0) | true (1) | true (1) | - // | true (1) | false (0) | true (1) | - // | true (1) | true (1) | false (0) | - // |------------------------------------| - - at_newline &= is_wspace; - if (!at_newline) { - saw_text = true; - } - } - - advance(lexer); - } - - lexer->result_symbol = JSX_TEXT; - return saw_text; -} - -bool tree_sitter_javascript_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { - if (valid_symbols[TEMPLATE_CHARS]) { - if (valid_symbols[AUTOMATIC_SEMICOLON]) { - return false; - } - return scan_template_chars(lexer); - } - - if (valid_symbols[JSX_TEXT] && scan_jsx_text(lexer)) { - return true; - } - - if (valid_symbols[AUTOMATIC_SEMICOLON]) { - bool scanned_comment = false; - bool ret = scan_automatic_semicolon(lexer, !valid_symbols[LOGICAL_OR], &scanned_comment); - if (!ret && !scanned_comment && valid_symbols[TERNARY_QMARK] && lexer->lookahead == '?') { - return scan_ternary_qmark(lexer); - } - return ret; - } - - if (valid_symbols[TERNARY_QMARK]) { - return scan_ternary_qmark(lexer); - } - - if (valid_symbols[HTML_COMMENT] && !valid_symbols[LOGICAL_OR] && !valid_symbols[ESCAPE_SEQUENCE] && - !valid_symbols[REGEX_PATTERN]) { - return scan_html_comment(lexer); - } - - return false; -} diff --git a/Packages/Sources/TreeSitterScanners/csrc/lua_scanner.c b/Packages/Sources/TreeSitterScanners/csrc/lua_scanner.c deleted file mode 100644 index e257c2dc..00000000 --- a/Packages/Sources/TreeSitterScanners/csrc/lua_scanner.c +++ /dev/null @@ -1,195 +0,0 @@ -#include -#include "tree_sitter/alloc.h" -#include "tree_sitter/parser.h" -#include - -enum TokenType { - BLOCK_COMMENT_START, - BLOCK_COMMENT_CONTENT, - BLOCK_COMMENT_END, - - BLOCK_STRING_START, - BLOCK_STRING_CONTENT, - BLOCK_STRING_END, -}; - -static inline void consume(TSLexer *lexer) { lexer->advance(lexer, false); } - -static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } - -static inline bool consume_char(char c, TSLexer *lexer) { - if (lexer->lookahead != c) { - return false; - } - - consume(lexer); - return true; -} - -static inline uint8_t consume_and_count_char(char c, TSLexer *lexer) { - uint8_t count = 0; - while (lexer->lookahead == c) { - ++count; - consume(lexer); - } - return count; -} - -static inline void skip_whitespaces(TSLexer *lexer) { - while (iswspace(lexer->lookahead)) { - skip(lexer); - } -} - -typedef struct { - char ending_char; - uint8_t level_count; -} Scanner; - -static inline void reset_state(Scanner *scanner) { - scanner->ending_char = 0; - scanner->level_count = 0; -} - -void *tree_sitter_lua_external_scanner_create() { - Scanner *scanner = ts_calloc(1, sizeof(Scanner)); - return scanner; -} - -void tree_sitter_lua_external_scanner_destroy(void *payload) { - Scanner *scanner = (Scanner *)payload; - ts_free(scanner); -} - -unsigned tree_sitter_lua_external_scanner_serialize(void *payload, char *buffer) { - Scanner *scanner = (Scanner *)payload; - buffer[0] = scanner->ending_char; - buffer[1] = (char)scanner->level_count; - return 2; -} - -void tree_sitter_lua_external_scanner_deserialize(void *payload, const char *buffer, unsigned length) { - Scanner *scanner = (Scanner *)payload; - if (length == 0) return; - scanner->ending_char = buffer[0]; - if (length == 1) return; - scanner->level_count = buffer[1]; -} - -static bool scan_block_start(Scanner *scanner, TSLexer *lexer) { - if (consume_char('[', lexer)) { - uint8_t level = consume_and_count_char('=', lexer); - - if (consume_char('[', lexer)) { - scanner->level_count = level; - return true; - } - } - - return false; -} - -static bool scan_block_end(Scanner *scanner, TSLexer *lexer) { - if (consume_char(']', lexer)) { - uint8_t level = consume_and_count_char('=', lexer); - - if (scanner->level_count == level && consume_char(']', lexer)) { - return true; - } - } - - return false; -} - -static bool scan_block_content(Scanner *scanner, TSLexer *lexer) { - while (lexer->lookahead != 0) { - if (lexer->lookahead == ']') { - lexer->mark_end(lexer); - - if (scan_block_end(scanner, lexer)) { - return true; - } - } else { - consume(lexer); - } - } - - return false; -} - -static bool scan_comment_start(Scanner *scanner, TSLexer *lexer) { - if (consume_char('-', lexer) && consume_char('-', lexer)) { - lexer->mark_end(lexer); - - if (scan_block_start(scanner, lexer)) { - lexer->mark_end(lexer); - lexer->result_symbol = BLOCK_COMMENT_START; - return true; - } - } - - return false; -} - -static bool scan_comment_content(Scanner *scanner, TSLexer *lexer) { - if (scanner->ending_char == 0) { // block comment - if (scan_block_content(scanner, lexer)) { - lexer->result_symbol = BLOCK_COMMENT_CONTENT; - return true; - } - - return false; - } - - while (lexer->lookahead != 0) { - if (lexer->lookahead == scanner->ending_char) { - reset_state(scanner); - lexer->result_symbol = BLOCK_COMMENT_CONTENT; - return true; - } - - consume(lexer); - } - - return false; -} - -bool tree_sitter_lua_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { - Scanner *scanner = (Scanner *)payload; - - if (valid_symbols[BLOCK_STRING_END] && scan_block_end(scanner, lexer)) { - reset_state(scanner); - lexer->result_symbol = BLOCK_STRING_END; - return true; - } - - if (valid_symbols[BLOCK_STRING_CONTENT] && scan_block_content(scanner, lexer)) { - lexer->result_symbol = BLOCK_STRING_CONTENT; - return true; - } - - if (valid_symbols[BLOCK_COMMENT_END] && scanner->ending_char == 0 && scan_block_end(scanner, lexer)) { - reset_state(scanner); - lexer->result_symbol = BLOCK_COMMENT_END; - return true; - } - - if (valid_symbols[BLOCK_COMMENT_CONTENT] && scan_comment_content(scanner, lexer)) { - return true; - } - - skip_whitespaces(lexer); - - if (valid_symbols[BLOCK_STRING_START] && scan_block_start(scanner, lexer)) { - lexer->result_symbol = BLOCK_STRING_START; - return true; - } - - if (valid_symbols[BLOCK_COMMENT_START]) { - if (scan_comment_start(scanner, lexer)) { - return true; - } - } - - return false; -} diff --git a/Packages/Sources/TreeSitterScanners/csrc/python_scanner.c b/Packages/Sources/TreeSitterScanners/csrc/python_scanner.c deleted file mode 100644 index 1fc77cdb..00000000 --- a/Packages/Sources/TreeSitterScanners/csrc/python_scanner.c +++ /dev/null @@ -1,437 +0,0 @@ -#include "tree_sitter/array.h" -#include "tree_sitter/parser.h" - -#include -#include -#include -#include - -enum TokenType { - NEWLINE, - INDENT, - DEDENT, - STRING_START, - STRING_CONTENT, - ESCAPE_INTERPOLATION, - STRING_END, - COMMENT, - CLOSE_PAREN, - CLOSE_BRACKET, - CLOSE_BRACE, - EXCEPT, -}; - -typedef enum { - SingleQuote = 1 << 0, - DoubleQuote = 1 << 1, - BackQuote = 1 << 2, - Raw = 1 << 3, - Format = 1 << 4, - Triple = 1 << 5, - Bytes = 1 << 6, -} Flags; - -typedef struct { - char flags; -} Delimiter; - -static inline Delimiter new_delimiter() { return (Delimiter){0}; } - -static inline bool is_format(Delimiter *delimiter) { return delimiter->flags & Format; } - -static inline bool is_raw(Delimiter *delimiter) { return delimiter->flags & Raw; } - -static inline bool is_triple(Delimiter *delimiter) { return delimiter->flags & Triple; } - -static inline bool is_bytes(Delimiter *delimiter) { return delimiter->flags & Bytes; } - -static inline int32_t end_character(Delimiter *delimiter) { - if (delimiter->flags & SingleQuote) { - return '\''; - } - if (delimiter->flags & DoubleQuote) { - return '"'; - } - if (delimiter->flags & BackQuote) { - return '`'; - } - return 0; -} - -static inline void set_format(Delimiter *delimiter) { delimiter->flags |= Format; } - -static inline void set_raw(Delimiter *delimiter) { delimiter->flags |= Raw; } - -static inline void set_triple(Delimiter *delimiter) { delimiter->flags |= Triple; } - -static inline void set_bytes(Delimiter *delimiter) { delimiter->flags |= Bytes; } - -static inline void set_end_character(Delimiter *delimiter, int32_t character) { - switch (character) { - case '\'': - delimiter->flags |= SingleQuote; - break; - case '"': - delimiter->flags |= DoubleQuote; - break; - case '`': - delimiter->flags |= BackQuote; - break; - default: - assert(false); - } -} - -typedef struct { - Array(uint16_t) indents; - Array(Delimiter) delimiters; - bool inside_interpolated_string; -} Scanner; - -static inline void advance(TSLexer *lexer) { lexer->advance(lexer, false); } - -static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } - -bool tree_sitter_python_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { - Scanner *scanner = (Scanner *)payload; - - bool error_recovery_mode = valid_symbols[STRING_CONTENT] && valid_symbols[INDENT]; - bool within_brackets = valid_symbols[CLOSE_BRACE] || valid_symbols[CLOSE_PAREN] || valid_symbols[CLOSE_BRACKET]; - - bool advanced_once = false; - if (valid_symbols[ESCAPE_INTERPOLATION] && scanner->delimiters.size > 0 && - (lexer->lookahead == '{' || lexer->lookahead == '}') && !error_recovery_mode) { - Delimiter *delimiter = array_back(&scanner->delimiters); - if (is_format(delimiter)) { - lexer->mark_end(lexer); - bool is_left_brace = lexer->lookahead == '{'; - advance(lexer); - advanced_once = true; - if ((lexer->lookahead == '{' && is_left_brace) || (lexer->lookahead == '}' && !is_left_brace)) { - advance(lexer); - lexer->mark_end(lexer); - lexer->result_symbol = ESCAPE_INTERPOLATION; - return true; - } - return false; - } - } - - if (valid_symbols[STRING_CONTENT] && scanner->delimiters.size > 0 && !error_recovery_mode) { - Delimiter *delimiter = array_back(&scanner->delimiters); - int32_t end_char = end_character(delimiter); - bool has_content = advanced_once; - while (lexer->lookahead) { - if ((advanced_once || lexer->lookahead == '{' || lexer->lookahead == '}') && is_format(delimiter)) { - lexer->mark_end(lexer); - lexer->result_symbol = STRING_CONTENT; - return has_content; - } - if (lexer->lookahead == '\\') { - if (is_raw(delimiter)) { - // Step over the backslash. - advance(lexer); - // Step over any escaped quotes. - if (lexer->lookahead == end_character(delimiter) || lexer->lookahead == '\\') { - advance(lexer); - } - // Step over newlines - if (lexer->lookahead == '\r') { - advance(lexer); - if (lexer->lookahead == '\n') { - advance(lexer); - } - } else if (lexer->lookahead == '\n') { - advance(lexer); - } - continue; - } - if (is_bytes(delimiter)) { - lexer->mark_end(lexer); - advance(lexer); - if (lexer->lookahead == 'N' || lexer->lookahead == 'u' || lexer->lookahead == 'U') { - // In bytes string, \N{...}, \uXXXX and \UXXXXXXXX are - // not escape sequences - // https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals - advance(lexer); - } else { - lexer->result_symbol = STRING_CONTENT; - return has_content; - } - } else { - lexer->mark_end(lexer); - lexer->result_symbol = STRING_CONTENT; - return has_content; - } - } else if (lexer->lookahead == end_char) { - if (is_triple(delimiter)) { - lexer->mark_end(lexer); - advance(lexer); - if (lexer->lookahead == end_char) { - advance(lexer); - if (lexer->lookahead == end_char) { - if (has_content) { - lexer->result_symbol = STRING_CONTENT; - } else { - advance(lexer); - lexer->mark_end(lexer); - array_pop(&scanner->delimiters); - lexer->result_symbol = STRING_END; - scanner->inside_interpolated_string = false; - } - return true; - } - lexer->mark_end(lexer); - lexer->result_symbol = STRING_CONTENT; - return true; - } - lexer->mark_end(lexer); - lexer->result_symbol = STRING_CONTENT; - return true; - } - if (has_content) { - lexer->result_symbol = STRING_CONTENT; - } else { - advance(lexer); - array_pop(&scanner->delimiters); - lexer->result_symbol = STRING_END; - scanner->inside_interpolated_string = false; - } - lexer->mark_end(lexer); - return true; - - } else if (lexer->lookahead == '\n' && has_content && !is_triple(delimiter)) { - return false; - } - advance(lexer); - has_content = true; - } - } - - lexer->mark_end(lexer); - - bool found_end_of_line = false; - uint16_t indent_length = 0; - int32_t first_comment_indent_length = -1; - for (;;) { - if (lexer->lookahead == '\n') { - found_end_of_line = true; - indent_length = 0; - skip(lexer); - } else if (lexer->lookahead == ' ') { - indent_length++; - skip(lexer); - } else if (lexer->lookahead == '\r' || lexer->lookahead == '\f') { - indent_length = 0; - skip(lexer); - } else if (lexer->lookahead == '\t') { - indent_length += 8; - skip(lexer); - } else if (lexer->lookahead == '#' && (valid_symbols[INDENT] || valid_symbols[DEDENT] || - valid_symbols[NEWLINE] || valid_symbols[EXCEPT])) { - // If we haven't found an EOL yet, - // then this is a comment after an expression: - // foo = bar # comment - // Just return, since we don't want to generate an indent/dedent - // token. - if (!found_end_of_line) { - return false; - } - if (first_comment_indent_length == -1) { - first_comment_indent_length = (int32_t)indent_length; - } - while (lexer->lookahead && lexer->lookahead != '\n') { - skip(lexer); - } - skip(lexer); - indent_length = 0; - } else if (lexer->lookahead == '\\') { - skip(lexer); - if (lexer->lookahead == '\r') { - skip(lexer); - } - if (lexer->lookahead == '\n' || lexer->eof(lexer)) { - skip(lexer); - } else { - return false; - } - } else if (lexer->eof(lexer)) { - indent_length = 0; - found_end_of_line = true; - break; - } else { - break; - } - } - - if (found_end_of_line) { - if (scanner->indents.size > 0) { - uint16_t current_indent_length = *array_back(&scanner->indents); - - if (valid_symbols[INDENT] && indent_length > current_indent_length) { - array_push(&scanner->indents, indent_length); - lexer->result_symbol = INDENT; - return true; - } - - bool next_tok_is_string_start = - lexer->lookahead == '\"' || lexer->lookahead == '\'' || lexer->lookahead == '`'; - - if ((valid_symbols[DEDENT] || - (!valid_symbols[NEWLINE] && !(valid_symbols[STRING_START] && next_tok_is_string_start) && - !within_brackets)) && - indent_length < current_indent_length && !scanner->inside_interpolated_string && - - // Wait to create a dedent token until we've consumed any - // comments - // whose indentation matches the current block. - first_comment_indent_length < (int32_t)current_indent_length) { - array_pop(&scanner->indents); - lexer->result_symbol = DEDENT; - return true; - } - } - - if (valid_symbols[NEWLINE] && !error_recovery_mode) { - lexer->result_symbol = NEWLINE; - return true; - } - } - - if (first_comment_indent_length == -1 && valid_symbols[STRING_START]) { - Delimiter delimiter = new_delimiter(); - - bool has_flags = false; - while (lexer->lookahead) { - if (lexer->lookahead == 'f' || lexer->lookahead == 'F' || lexer->lookahead == 't' || - lexer->lookahead == 'T') { - set_format(&delimiter); - } else if (lexer->lookahead == 'r' || lexer->lookahead == 'R') { - set_raw(&delimiter); - } else if (lexer->lookahead == 'b' || lexer->lookahead == 'B') { - set_bytes(&delimiter); - } else if (lexer->lookahead != 'u' && lexer->lookahead != 'U') { - break; - } - has_flags = true; - advance(lexer); - } - - if (lexer->lookahead == '`') { - set_end_character(&delimiter, '`'); - advance(lexer); - lexer->mark_end(lexer); - } else if (lexer->lookahead == '\'') { - set_end_character(&delimiter, '\''); - advance(lexer); - lexer->mark_end(lexer); - if (lexer->lookahead == '\'') { - advance(lexer); - if (lexer->lookahead == '\'') { - advance(lexer); - lexer->mark_end(lexer); - set_triple(&delimiter); - } - } - } else if (lexer->lookahead == '"') { - set_end_character(&delimiter, '"'); - advance(lexer); - lexer->mark_end(lexer); - if (lexer->lookahead == '"') { - advance(lexer); - if (lexer->lookahead == '"') { - advance(lexer); - lexer->mark_end(lexer); - set_triple(&delimiter); - } - } - } - - if (end_character(&delimiter)) { - array_push(&scanner->delimiters, delimiter); - lexer->result_symbol = STRING_START; - scanner->inside_interpolated_string = is_format(&delimiter); - return true; - } - if (has_flags) { - return false; - } - } - - return false; -} - -unsigned tree_sitter_python_external_scanner_serialize(void *payload, char *buffer) { - Scanner *scanner = (Scanner *)payload; - - size_t size = 0; - - buffer[size++] = (char)scanner->inside_interpolated_string; - - size_t delimiter_count = scanner->delimiters.size; - if (delimiter_count > UINT8_MAX) { - delimiter_count = UINT8_MAX; - } - buffer[size++] = (char)delimiter_count; - - if (delimiter_count > 0) { - memcpy(&buffer[size], scanner->delimiters.contents, delimiter_count); - } - size += delimiter_count; - - uint32_t iter = 1; - for (; iter < scanner->indents.size && size < TREE_SITTER_SERIALIZATION_BUFFER_SIZE; ++iter) { - uint16_t indent_value = *array_get(&scanner->indents, iter); - buffer[size++] = (char)(indent_value & 0xFF); - buffer[size++] = (char)((indent_value >> 8) & 0xFF); - } - - return size; -} - -void tree_sitter_python_external_scanner_deserialize(void *payload, const char *buffer, unsigned length) { - Scanner *scanner = (Scanner *)payload; - - array_delete(&scanner->delimiters); - array_delete(&scanner->indents); - array_push(&scanner->indents, 0); - - if (length > 0) { - size_t size = 0; - - scanner->inside_interpolated_string = (bool)buffer[size++]; - - size_t delimiter_count = (uint8_t)buffer[size++]; - if (delimiter_count > 0) { - array_reserve(&scanner->delimiters, delimiter_count); - scanner->delimiters.size = delimiter_count; - memcpy(scanner->delimiters.contents, &buffer[size], delimiter_count); - size += delimiter_count; - } - - for (; size + 1 < length; size += 2) { - uint16_t indent_value = (unsigned char)buffer[size] | ((unsigned char)buffer[size + 1] << 8); - array_push(&scanner->indents, indent_value); - } - } -} - -void *tree_sitter_python_external_scanner_create() { -#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) - _Static_assert(sizeof(Delimiter) == sizeof(char), ""); -#else - assert(sizeof(Delimiter) == sizeof(char)); -#endif - Scanner *scanner = calloc(1, sizeof(Scanner)); - array_init(&scanner->indents); - array_init(&scanner->delimiters); - tree_sitter_python_external_scanner_deserialize(scanner, NULL, 0); - return scanner; -} - -void tree_sitter_python_external_scanner_destroy(void *payload) { - Scanner *scanner = (Scanner *)payload; - array_delete(&scanner->indents); - array_delete(&scanner->delimiters); - free(scanner); -} diff --git a/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/alloc.h b/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/alloc.h deleted file mode 100644 index 1abdd120..00000000 --- a/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/alloc.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef TREE_SITTER_ALLOC_H_ -#define TREE_SITTER_ALLOC_H_ - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include - -// Allow clients to override allocation functions -#ifdef TREE_SITTER_REUSE_ALLOCATOR - -extern void *(*ts_current_malloc)(size_t size); -extern void *(*ts_current_calloc)(size_t count, size_t size); -extern void *(*ts_current_realloc)(void *ptr, size_t size); -extern void (*ts_current_free)(void *ptr); - -#ifndef ts_malloc -#define ts_malloc ts_current_malloc -#endif -#ifndef ts_calloc -#define ts_calloc ts_current_calloc -#endif -#ifndef ts_realloc -#define ts_realloc ts_current_realloc -#endif -#ifndef ts_free -#define ts_free ts_current_free -#endif - -#else - -#ifndef ts_malloc -#define ts_malloc malloc -#endif -#ifndef ts_calloc -#define ts_calloc calloc -#endif -#ifndef ts_realloc -#define ts_realloc realloc -#endif -#ifndef ts_free -#define ts_free free -#endif - -#endif - -#ifdef __cplusplus -} -#endif - -#endif // TREE_SITTER_ALLOC_H_ diff --git a/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/array.h b/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/array.h deleted file mode 100644 index a17a574f..00000000 --- a/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/array.h +++ /dev/null @@ -1,291 +0,0 @@ -#ifndef TREE_SITTER_ARRAY_H_ -#define TREE_SITTER_ARRAY_H_ - -#ifdef __cplusplus -extern "C" { -#endif - -#include "./alloc.h" - -#include -#include -#include -#include -#include - -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable : 4101) -#elif defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-variable" -#endif - -#define Array(T) \ - struct { \ - T *contents; \ - uint32_t size; \ - uint32_t capacity; \ - } - -/// Initialize an array. -#define array_init(self) \ - ((self)->size = 0, (self)->capacity = 0, (self)->contents = NULL) - -/// Create an empty array. -#define array_new() \ - { NULL, 0, 0 } - -/// Get a pointer to the element at a given `index` in the array. -#define array_get(self, _index) \ - (assert((uint32_t)(_index) < (self)->size), &(self)->contents[_index]) - -/// Get a pointer to the first element in the array. -#define array_front(self) array_get(self, 0) - -/// Get a pointer to the last element in the array. -#define array_back(self) array_get(self, (self)->size - 1) - -/// Clear the array, setting its size to zero. Note that this does not free any -/// memory allocated for the array's contents. -#define array_clear(self) ((self)->size = 0) - -/// Reserve `new_capacity` elements of space in the array. If `new_capacity` is -/// less than the array's current capacity, this function has no effect. -#define array_reserve(self, new_capacity) \ - _array__reserve((Array *)(self), array_elem_size(self), new_capacity) - -/// Free any memory allocated for this array. Note that this does not free any -/// memory allocated for the array's contents. -#define array_delete(self) _array__delete((Array *)(self)) - -/// Push a new `element` onto the end of the array. -#define array_push(self, element) \ - (_array__grow((Array *)(self), 1, array_elem_size(self)), \ - (self)->contents[(self)->size++] = (element)) - -/// Increase the array's size by `count` elements. -/// New elements are zero-initialized. -#define array_grow_by(self, count) \ - do { \ - if ((count) == 0) break; \ - _array__grow((Array *)(self), count, array_elem_size(self)); \ - memset((self)->contents + (self)->size, 0, (count) * array_elem_size(self)); \ - (self)->size += (count); \ - } while (0) - -/// Append all elements from one array to the end of another. -#define array_push_all(self, other) \ - array_extend((self), (other)->size, (other)->contents) - -/// Append `count` elements to the end of the array, reading their values from the -/// `contents` pointer. -#define array_extend(self, count, contents) \ - _array__splice( \ - (Array *)(self), array_elem_size(self), (self)->size, \ - 0, count, contents \ - ) - -/// Remove `old_count` elements from the array starting at the given `index`. At -/// the same index, insert `new_count` new elements, reading their values from the -/// `new_contents` pointer. -#define array_splice(self, _index, old_count, new_count, new_contents) \ - _array__splice( \ - (Array *)(self), array_elem_size(self), _index, \ - old_count, new_count, new_contents \ - ) - -/// Insert one `element` into the array at the given `index`. -#define array_insert(self, _index, element) \ - _array__splice((Array *)(self), array_elem_size(self), _index, 0, 1, &(element)) - -/// Remove one element from the array at the given `index`. -#define array_erase(self, _index) \ - _array__erase((Array *)(self), array_elem_size(self), _index) - -/// Pop the last element off the array, returning the element by value. -#define array_pop(self) ((self)->contents[--(self)->size]) - -/// Assign the contents of one array to another, reallocating if necessary. -#define array_assign(self, other) \ - _array__assign((Array *)(self), (const Array *)(other), array_elem_size(self)) - -/// Swap one array with another -#define array_swap(self, other) \ - _array__swap((Array *)(self), (Array *)(other)) - -/// Get the size of the array contents -#define array_elem_size(self) (sizeof *(self)->contents) - -/// Search a sorted array for a given `needle` value, using the given `compare` -/// callback to determine the order. -/// -/// If an existing element is found to be equal to `needle`, then the `index` -/// out-parameter is set to the existing value's index, and the `exists` -/// out-parameter is set to true. Otherwise, `index` is set to an index where -/// `needle` should be inserted in order to preserve the sorting, and `exists` -/// is set to false. -#define array_search_sorted_with(self, compare, needle, _index, _exists) \ - _array__search_sorted(self, 0, compare, , needle, _index, _exists) - -/// Search a sorted array for a given `needle` value, using integer comparisons -/// of a given struct field (specified with a leading dot) to determine the order. -/// -/// See also `array_search_sorted_with`. -#define array_search_sorted_by(self, field, needle, _index, _exists) \ - _array__search_sorted(self, 0, _compare_int, field, needle, _index, _exists) - -/// Insert a given `value` into a sorted array, using the given `compare` -/// callback to determine the order. -#define array_insert_sorted_with(self, compare, value) \ - do { \ - unsigned _index, _exists; \ - array_search_sorted_with(self, compare, &(value), &_index, &_exists); \ - if (!_exists) array_insert(self, _index, value); \ - } while (0) - -/// Insert a given `value` into a sorted array, using integer comparisons of -/// a given struct field (specified with a leading dot) to determine the order. -/// -/// See also `array_search_sorted_by`. -#define array_insert_sorted_by(self, field, value) \ - do { \ - unsigned _index, _exists; \ - array_search_sorted_by(self, field, (value) field, &_index, &_exists); \ - if (!_exists) array_insert(self, _index, value); \ - } while (0) - -// Private - -typedef Array(void) Array; - -/// This is not what you're looking for, see `array_delete`. -static inline void _array__delete(Array *self) { - if (self->contents) { - ts_free(self->contents); - self->contents = NULL; - self->size = 0; - self->capacity = 0; - } -} - -/// This is not what you're looking for, see `array_erase`. -static inline void _array__erase(Array *self, size_t element_size, - uint32_t index) { - assert(index < self->size); - char *contents = (char *)self->contents; - memmove(contents + index * element_size, contents + (index + 1) * element_size, - (self->size - index - 1) * element_size); - self->size--; -} - -/// This is not what you're looking for, see `array_reserve`. -static inline void _array__reserve(Array *self, size_t element_size, uint32_t new_capacity) { - if (new_capacity > self->capacity) { - if (self->contents) { - self->contents = ts_realloc(self->contents, new_capacity * element_size); - } else { - self->contents = ts_malloc(new_capacity * element_size); - } - self->capacity = new_capacity; - } -} - -/// This is not what you're looking for, see `array_assign`. -static inline void _array__assign(Array *self, const Array *other, size_t element_size) { - _array__reserve(self, element_size, other->size); - self->size = other->size; - memcpy(self->contents, other->contents, self->size * element_size); -} - -/// This is not what you're looking for, see `array_swap`. -static inline void _array__swap(Array *self, Array *other) { - Array swap = *other; - *other = *self; - *self = swap; -} - -/// This is not what you're looking for, see `array_push` or `array_grow_by`. -static inline void _array__grow(Array *self, uint32_t count, size_t element_size) { - uint32_t new_size = self->size + count; - if (new_size > self->capacity) { - uint32_t new_capacity = self->capacity * 2; - if (new_capacity < 8) new_capacity = 8; - if (new_capacity < new_size) new_capacity = new_size; - _array__reserve(self, element_size, new_capacity); - } -} - -/// This is not what you're looking for, see `array_splice`. -static inline void _array__splice(Array *self, size_t element_size, - uint32_t index, uint32_t old_count, - uint32_t new_count, const void *elements) { - uint32_t new_size = self->size + new_count - old_count; - uint32_t old_end = index + old_count; - uint32_t new_end = index + new_count; - assert(old_end <= self->size); - - _array__reserve(self, element_size, new_size); - - char *contents = (char *)self->contents; - if (self->size > old_end) { - memmove( - contents + new_end * element_size, - contents + old_end * element_size, - (self->size - old_end) * element_size - ); - } - if (new_count > 0) { - if (elements) { - memcpy( - (contents + index * element_size), - elements, - new_count * element_size - ); - } else { - memset( - (contents + index * element_size), - 0, - new_count * element_size - ); - } - } - self->size += new_count - old_count; -} - -/// A binary search routine, based on Rust's `std::slice::binary_search_by`. -/// This is not what you're looking for, see `array_search_sorted_with` or `array_search_sorted_by`. -#define _array__search_sorted(self, start, compare, suffix, needle, _index, _exists) \ - do { \ - *(_index) = start; \ - *(_exists) = false; \ - uint32_t size = (self)->size - *(_index); \ - if (size == 0) break; \ - int comparison; \ - while (size > 1) { \ - uint32_t half_size = size / 2; \ - uint32_t mid_index = *(_index) + half_size; \ - comparison = compare(&((self)->contents[mid_index] suffix), (needle)); \ - if (comparison <= 0) *(_index) = mid_index; \ - size -= half_size; \ - } \ - comparison = compare(&((self)->contents[*(_index)] suffix), (needle)); \ - if (comparison == 0) *(_exists) = true; \ - else if (comparison < 0) *(_index) += 1; \ - } while (0) - -/// Helper macro for the `_sorted_by` routines below. This takes the left (existing) -/// parameter by reference in order to work with the generic sorting function above. -#define _compare_int(a, b) ((int)*(a) - (int)(b)) - -#ifdef _MSC_VER -#pragma warning(pop) -#elif defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic pop -#endif - -#ifdef __cplusplus -} -#endif - -#endif // TREE_SITTER_ARRAY_H_ diff --git a/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/parser.h b/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/parser.h deleted file mode 100644 index 858107de..00000000 --- a/Packages/Sources/TreeSitterScanners/csrc/tree_sitter/parser.h +++ /dev/null @@ -1,286 +0,0 @@ -#ifndef TREE_SITTER_PARSER_H_ -#define TREE_SITTER_PARSER_H_ - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include - -#define ts_builtin_sym_error ((TSSymbol)-1) -#define ts_builtin_sym_end 0 -#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024 - -#ifndef TREE_SITTER_API_H_ -typedef uint16_t TSStateId; -typedef uint16_t TSSymbol; -typedef uint16_t TSFieldId; -typedef struct TSLanguage TSLanguage; -typedef struct TSLanguageMetadata { - uint8_t major_version; - uint8_t minor_version; - uint8_t patch_version; -} TSLanguageMetadata; -#endif - -typedef struct { - TSFieldId field_id; - uint8_t child_index; - bool inherited; -} TSFieldMapEntry; - -// Used to index the field and supertype maps. -typedef struct { - uint16_t index; - uint16_t length; -} TSMapSlice; - -typedef struct { - bool visible; - bool named; - bool supertype; -} TSSymbolMetadata; - -typedef struct TSLexer TSLexer; - -struct TSLexer { - int32_t lookahead; - TSSymbol result_symbol; - void (*advance)(TSLexer *, bool); - void (*mark_end)(TSLexer *); - uint32_t (*get_column)(TSLexer *); - bool (*is_at_included_range_start)(const TSLexer *); - bool (*eof)(const TSLexer *); - void (*log)(const TSLexer *, const char *, ...); -}; - -typedef enum { - TSParseActionTypeShift, - TSParseActionTypeReduce, - TSParseActionTypeAccept, - TSParseActionTypeRecover, -} TSParseActionType; - -typedef union { - struct { - uint8_t type; - TSStateId state; - bool extra; - bool repetition; - } shift; - struct { - uint8_t type; - uint8_t child_count; - TSSymbol symbol; - int16_t dynamic_precedence; - uint16_t production_id; - } reduce; - uint8_t type; -} TSParseAction; - -typedef struct { - uint16_t lex_state; - uint16_t external_lex_state; -} TSLexMode; - -typedef struct { - uint16_t lex_state; - uint16_t external_lex_state; - uint16_t reserved_word_set_id; -} TSLexerMode; - -typedef union { - TSParseAction action; - struct { - uint8_t count; - bool reusable; - } entry; -} TSParseActionEntry; - -typedef struct { - int32_t start; - int32_t end; -} TSCharacterRange; - -struct TSLanguage { - uint32_t abi_version; - uint32_t symbol_count; - uint32_t alias_count; - uint32_t token_count; - uint32_t external_token_count; - uint32_t state_count; - uint32_t large_state_count; - uint32_t production_id_count; - uint32_t field_count; - uint16_t max_alias_sequence_length; - const uint16_t *parse_table; - const uint16_t *small_parse_table; - const uint32_t *small_parse_table_map; - const TSParseActionEntry *parse_actions; - const char * const *symbol_names; - const char * const *field_names; - const TSMapSlice *field_map_slices; - const TSFieldMapEntry *field_map_entries; - const TSSymbolMetadata *symbol_metadata; - const TSSymbol *public_symbol_map; - const uint16_t *alias_map; - const TSSymbol *alias_sequences; - const TSLexerMode *lex_modes; - bool (*lex_fn)(TSLexer *, TSStateId); - bool (*keyword_lex_fn)(TSLexer *, TSStateId); - TSSymbol keyword_capture_token; - struct { - const bool *states; - const TSSymbol *symbol_map; - void *(*create)(void); - void (*destroy)(void *); - bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist); - unsigned (*serialize)(void *, char *); - void (*deserialize)(void *, const char *, unsigned); - } external_scanner; - const TSStateId *primary_state_ids; - const char *name; - const TSSymbol *reserved_words; - uint16_t max_reserved_word_set_size; - uint32_t supertype_count; - const TSSymbol *supertype_symbols; - const TSMapSlice *supertype_map_slices; - const TSSymbol *supertype_map_entries; - TSLanguageMetadata metadata; -}; - -static inline bool set_contains(const TSCharacterRange *ranges, uint32_t len, int32_t lookahead) { - uint32_t index = 0; - uint32_t size = len - index; - while (size > 1) { - uint32_t half_size = size / 2; - uint32_t mid_index = index + half_size; - const TSCharacterRange *range = &ranges[mid_index]; - if (lookahead >= range->start && lookahead <= range->end) { - return true; - } else if (lookahead > range->end) { - index = mid_index; - } - size -= half_size; - } - const TSCharacterRange *range = &ranges[index]; - return (lookahead >= range->start && lookahead <= range->end); -} - -/* - * Lexer Macros - */ - -#ifdef _MSC_VER -#define UNUSED __pragma(warning(suppress : 4101)) -#else -#define UNUSED __attribute__((unused)) -#endif - -#define START_LEXER() \ - bool result = false; \ - bool skip = false; \ - UNUSED \ - bool eof = false; \ - int32_t lookahead; \ - goto start; \ - next_state: \ - lexer->advance(lexer, skip); \ - start: \ - skip = false; \ - lookahead = lexer->lookahead; - -#define ADVANCE(state_value) \ - { \ - state = state_value; \ - goto next_state; \ - } - -#define ADVANCE_MAP(...) \ - { \ - static const uint16_t map[] = { __VA_ARGS__ }; \ - for (uint32_t i = 0; i < sizeof(map) / sizeof(map[0]); i += 2) { \ - if (map[i] == lookahead) { \ - state = map[i + 1]; \ - goto next_state; \ - } \ - } \ - } - -#define SKIP(state_value) \ - { \ - skip = true; \ - state = state_value; \ - goto next_state; \ - } - -#define ACCEPT_TOKEN(symbol_value) \ - result = true; \ - lexer->result_symbol = symbol_value; \ - lexer->mark_end(lexer); - -#define END_STATE() return result; - -/* - * Parse Table Macros - */ - -#define SMALL_STATE(id) ((id) - LARGE_STATE_COUNT) - -#define STATE(id) id - -#define ACTIONS(id) id - -#define SHIFT(state_value) \ - {{ \ - .shift = { \ - .type = TSParseActionTypeShift, \ - .state = (state_value) \ - } \ - }} - -#define SHIFT_REPEAT(state_value) \ - {{ \ - .shift = { \ - .type = TSParseActionTypeShift, \ - .state = (state_value), \ - .repetition = true \ - } \ - }} - -#define SHIFT_EXTRA() \ - {{ \ - .shift = { \ - .type = TSParseActionTypeShift, \ - .extra = true \ - } \ - }} - -#define REDUCE(symbol_name, children, precedence, prod_id) \ - {{ \ - .reduce = { \ - .type = TSParseActionTypeReduce, \ - .symbol = symbol_name, \ - .child_count = children, \ - .dynamic_precedence = precedence, \ - .production_id = prod_id \ - }, \ - }} - -#define RECOVER() \ - {{ \ - .type = TSParseActionTypeRecover \ - }} - -#define ACCEPT_INPUT() \ - {{ \ - .type = TSParseActionTypeAccept \ - }} - -#ifdef __cplusplus -} -#endif - -#endif // TREE_SITTER_PARSER_H_ diff --git a/Packages/Sources/TreeSitterScanners/include/treesitter_scanners.h b/Packages/Sources/TreeSitterScanners/include/treesitter_scanners.h deleted file mode 100644 index 6750bb86..00000000 --- a/Packages/Sources/TreeSitterScanners/include/treesitter_scanners.h +++ /dev/null @@ -1,2 +0,0 @@ -// Intentionally empty. This target only provides external-scanner -// symbols for grammar packages whose SPM manifest omits scanner.c. diff --git a/RxCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/RxCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 07bfc140..485ec6d8 100644 --- a/RxCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/RxCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,465 +1,240 @@ { - "originHash" : "4e57c3e72b3783f86f3921d55aadf023d63038ca5bc9265e34900dc7724ab193", - "pins" : [ + "originHash": "4e57c3e72b3783f86f3921d55aadf023d63038ca5bc9265e34900dc7724ab193", + "pins": [ { - "identity" : "abseil-cpp-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/abseil-cpp-binary.git", - "state" : { - "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", - "version" : "1.2024072200.0" + "identity": "abseil-cpp-binary", + "kind": "remoteSourceControl", + "location": "https://github.com/google/abseil-cpp-binary.git", + "state": { + "revision": "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version": "1.2024072200.0" } }, { - "identity" : "app-check", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/app-check.git", - "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" + "identity": "app-check", + "kind": "remoteSourceControl", + "location": "https://github.com/google/app-check.git", + "state": { + "revision": "61b85103a1aeed8218f17c794687781505fbbef5", + "version": "11.2.0" } }, { - "identity" : "firebase-ios-sdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/firebase-ios-sdk", - "state" : { - "revision" : "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5", - "version" : "11.15.0" + "identity": "firebase-ios-sdk", + "kind": "remoteSourceControl", + "location": "https://github.com/firebase/firebase-ios-sdk", + "state": { + "revision": "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5", + "version": "11.15.0" } }, { - "identity" : "google-ads-on-device-conversion-ios-sdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", - "state" : { - "revision" : "a2d0f1f1666de591eb1a811f40b1706f5c63a2ed", - "version" : "2.3.0" + "identity": "google-ads-on-device-conversion-ios-sdk", + "kind": "remoteSourceControl", + "location": "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", + "state": { + "revision": "a2d0f1f1666de591eb1a811f40b1706f5c63a2ed", + "version": "2.3.0" } }, { - "identity" : "googleappmeasurement", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleAppMeasurement.git", - "state" : { - "revision" : "45ce435e9406d3c674dd249a042b932bee006f60", - "version" : "11.15.0" + "identity": "googleappmeasurement", + "kind": "remoteSourceControl", + "location": "https://github.com/google/GoogleAppMeasurement.git", + "state": { + "revision": "45ce435e9406d3c674dd249a042b932bee006f60", + "version": "11.15.0" } }, { - "identity" : "googledatatransport", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleDataTransport.git", - "state" : { - "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", - "version" : "10.1.0" + "identity": "googledatatransport", + "kind": "remoteSourceControl", + "location": "https://github.com/google/GoogleDataTransport.git", + "state": { + "revision": "617af071af9aa1d6a091d59a202910ac482128f9", + "version": "10.1.0" } }, { - "identity" : "googleutilities", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleUtilities.git", - "state" : { - "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", - "version" : "8.1.0" + "identity": "googleutilities", + "kind": "remoteSourceControl", + "location": "https://github.com/google/GoogleUtilities.git", + "state": { + "revision": "60da361632d0de02786f709bdc0c4df340f7613e", + "version": "8.1.0" } }, { - "identity" : "grpc-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/grpc-binary.git", - "state" : { - "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", - "version" : "1.69.1" + "identity": "grpc-binary", + "kind": "remoteSourceControl", + "location": "https://github.com/google/grpc-binary.git", + "state": { + "revision": "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version": "1.69.1" } }, { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "c756a29784521063b6a1202907e2cc47f41b667c", - "version" : "4.5.0" + "identity": "gtm-session-fetcher", + "kind": "remoteSourceControl", + "location": "https://github.com/google/gtm-session-fetcher.git", + "state": { + "revision": "c756a29784521063b6a1202907e2cc47f41b667c", + "version": "4.5.0" } }, { - "identity" : "interop-ios-for-google-sdks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/interop-ios-for-google-sdks.git", - "state" : { - "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", - "version" : "101.0.0" + "identity": "interop-ios-for-google-sdks", + "kind": "remoteSourceControl", + "location": "https://github.com/google/interop-ios-for-google-sdks.git", + "state": { + "revision": "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version": "101.0.0" } }, { - "identity" : "leveldb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/leveldb.git", - "state" : { - "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", - "version" : "1.22.5" + "identity": "leveldb", + "kind": "remoteSourceControl", + "location": "https://github.com/firebase/leveldb.git", + "state": { + "revision": "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version": "1.22.5" } }, { - "identity" : "nanopb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/nanopb.git", - "state" : { - "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", - "version" : "2.30910.0" + "identity": "nanopb", + "kind": "remoteSourceControl", + "location": "https://github.com/firebase/nanopb.git", + "state": { + "revision": "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version": "2.30910.0" } }, { - "identity" : "promises", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/promises.git", - "state" : { - "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version" : "2.4.0" + "identity": "promises", + "kind": "remoteSourceControl", + "location": "https://github.com/google/promises.git", + "state": { + "revision": "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version": "2.4.0" } }, { - "identity" : "rxauthswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/rxtech-lab/RxAuthSwift", - "state" : { - "revision" : "f1bc8a1004c58f7eae628eaf8ae705e4f8c21c51", - "version" : "1.1.1" + "identity": "rxauthswift", + "kind": "remoteSourceControl", + "location": "https://github.com/rxtech-lab/RxAuthSwift", + "state": { + "revision": "f1bc8a1004c58f7eae628eaf8ae705e4f8c21c51", + "version": "1.1.1" } }, { - "identity" : "sdwebimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SDWebImage/SDWebImage.git", - "state" : { - "revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0", - "version" : "5.21.7" + "identity": "sdwebimage", + "kind": "remoteSourceControl", + "location": "https://github.com/SDWebImage/SDWebImage.git", + "state": { + "revision": "2de3a496eaf6df9a1312862adcfd54acd73c39c0", + "version": "5.21.7" } }, { - "identity" : "sdwebimagesvgcoder", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SDWebImage/SDWebImageSVGCoder", - "state" : { - "revision" : "85b5d58ad02c207c496fa34426dc6560d6ae32f0", - "version" : "1.8.0" + "identity": "sdwebimagesvgcoder", + "kind": "remoteSourceControl", + "location": "https://github.com/SDWebImage/SDWebImageSVGCoder", + "state": { + "revision": "85b5d58ad02c207c496fa34426dc6560d6ae32f0", + "version": "1.8.0" } }, { - "identity" : "sparkle", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sparkle-project/Sparkle.git", - "state" : { - "revision" : "066e75a8b3e99962685d6a90cdd5293ebffd9261", - "version" : "2.9.1" + "identity": "sparkle", + "kind": "remoteSourceControl", + "location": "https://github.com/sparkle-project/Sparkle.git", + "state": { + "revision": "066e75a8b3e99962685d6a90cdd5293ebffd9261", + "version": "2.9.1" } }, { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", - "state" : { - "revision" : "626b5b7b2f45e1b0b1c6f4a309296d1d21d7311b", - "version" : "1.7.1" + "identity": "swift-argument-parser", + "kind": "remoteSourceControl", + "location": "https://github.com/apple/swift-argument-parser", + "state": { + "revision": "626b5b7b2f45e1b0b1c6f4a309296d1d21d7311b", + "version": "1.7.1" } }, { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "fea17c02d767f46b23070fdfdacc28a03a39232a", - "version" : "1.5.1" + "identity": "swift-collections", + "kind": "remoteSourceControl", + "location": "https://github.com/apple/swift-collections", + "state": { + "revision": "fea17c02d767f46b23070fdfdacc28a03a39232a", + "version": "1.5.1" } }, { - "identity" : "swift-json-schema", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sirily11/swift-json-schema", - "state" : { - "revision" : "663afab8c131151950fd7fb114871c02a529a4b4", - "version" : "1.0.2" + "identity": "swift-json-schema", + "kind": "remoteSourceControl", + "location": "https://github.com/sirily11/swift-json-schema", + "state": { + "revision": "663afab8c131151950fd7fb114871c02a529a4b4", + "version": "1.0.2" } }, { - "identity" : "swift-jsonschema-form", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sirily11/swift-jsonschema-form", - "state" : { - "branch" : "main", - "revision" : "a4feb400a0bca57bc39b8ea95544c71aa768fbfd" + "identity": "swift-jsonschema-form", + "kind": "remoteSourceControl", + "location": "https://github.com/sirily11/swift-jsonschema-form", + "state": { + "branch": "main", + "revision": "a4feb400a0bca57bc39b8ea95544c71aa768fbfd" } }, { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "7dc6101ae4dbe95cd3bc9cebad3b7cf8e49a7a63", - "version" : "1.13.0" + "identity": "swift-log", + "kind": "remoteSourceControl", + "location": "https://github.com/apple/swift-log.git", + "state": { + "revision": "7dc6101ae4dbe95cd3bc9cebad3b7cf8e49a7a63", + "version": "1.13.0" } }, { - "identity" : "swift-protobuf", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-protobuf.git", - "state" : { - "revision" : "f6506eaa86ed2e01cb0ae14a75035b7fdbf0918f", - "version" : "1.38.0" + "identity": "swift-protobuf", + "kind": "remoteSourceControl", + "location": "https://github.com/apple/swift-protobuf.git", + "state": { + "revision": "f6506eaa86ed2e01cb0ae14a75035b7fdbf0918f", + "version": "1.38.0" } }, { - "identity" : "swift-tree-sitter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/swift-tree-sitter", - "state" : { - "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", - "version" : "0.25.0" + "identity": "swiftterm", + "kind": "remoteSourceControl", + "location": "https://github.com/migueldeicaza/SwiftTerm.git", + "state": { + "revision": "8e7a1e154f470e19c709a00a8768df348ba5fc43", + "version": "1.13.0" } }, { - "identity" : "swiftterm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/migueldeicaza/SwiftTerm.git", - "state" : { - "revision" : "8e7a1e154f470e19c709a00a8768df348ba5fc43", - "version" : "1.13.0" + "identity": "viewinspector", + "kind": "remoteSourceControl", + "location": "https://github.com/nalexn/ViewInspector", + "state": { + "revision": "e9a06346499a3a889165647e3f23f8a7b2609a1c", + "version": "0.10.3" } }, { - "identity" : "tree-sitter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter", - "state" : { - "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406", - "version" : "0.25.10" - } - }, - { - "identity" : "tree-sitter-bash", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-bash", - "state" : { - "revision" : "a06c2e4415e9bc0346c6b86d401879ffb44058f7", - "version" : "0.25.1" - } - }, - { - "identity" : "tree-sitter-c", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-c", - "state" : { - "revision" : "b780e47fc780ddc8da13afa35a3f4ed5c157823d", - "version" : "0.24.2" - } - }, - { - "identity" : "tree-sitter-c-sharp", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-c-sharp", - "state" : { - "revision" : "cac6d5fb595f5811a076336682d5d595ac1c9e85", - "version" : "0.23.5" - } - }, - { - "identity" : "tree-sitter-cpp", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-cpp", - "state" : { - "revision" : "f41e1a044c8a84ea9fa8577fdd2eab92ec96de02", - "version" : "0.23.4" - } - }, - { - "identity" : "tree-sitter-css", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-css", - "state" : { - "revision" : "dda5cfc5722c429eaba1c910ca32c2c0c5bb1a3f", - "version" : "0.25.0" - } - }, - { - "identity" : "tree-sitter-go", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-go", - "state" : { - "revision" : "1547678a9da59885853f5f5cc8a99cc203fa2e2c", - "version" : "0.25.0" - } - }, - { - "identity" : "tree-sitter-haskell", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-haskell", - "state" : { - "revision" : "c30d812bc90827f1a54106a25bc9a6307f5cdcec", - "version" : "0.23.1" - } - }, - { - "identity" : "tree-sitter-html", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-html", - "state" : { - "revision" : "5a5ca8551a179998360b4a4ca2c0f366a35acc03", - "version" : "0.23.2" - } - }, - { - "identity" : "tree-sitter-java", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-java", - "state" : { - "revision" : "94703d5a6bed02b98e438d7cad1136c01a60ba2c", - "version" : "0.23.5" - } - }, - { - "identity" : "tree-sitter-javascript", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-javascript", - "state" : { - "revision" : "44c892e0be055ac465d5eeddae6d3e194424e7de", - "version" : "0.25.0" - } - }, - { - "identity" : "tree-sitter-json", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-json", - "state" : { - "revision" : "ee35a6ebefcef0c5c416c0d1ccec7370cfca5a24", - "version" : "0.24.8" - } - }, - { - "identity" : "tree-sitter-kotlin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/fwcd/tree-sitter-kotlin", - "state" : { - "revision" : "e1a2d5ad1f61f5740677183cd4125bb071cd2f30", - "version" : "0.3.8" - } - }, - { - "identity" : "tree-sitter-lua", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter-grammars/tree-sitter-lua", - "state" : { - "revision" : "10fe0054734eec83049514ea2e718b2a56acd0c9", - "version" : "0.5.0" - } - }, - { - "identity" : "tree-sitter-markdown", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter-grammars/tree-sitter-markdown", - "state" : { - "revision" : "f969cd3ae3f9fbd4e43205431d0ae286014c05b5", - "version" : "0.5.3" - } - }, - { - "identity" : "tree-sitter-php", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-php", - "state" : { - "revision" : "5b5627faaa290d89eb3d01b9bf47c3bb9e797dea", - "version" : "0.24.2" - } - }, - { - "identity" : "tree-sitter-python", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-python", - "state" : { - "revision" : "293fdc02038ee2bf0e2e206711b69c90ac0d413f", - "version" : "0.25.0" - } - }, - { - "identity" : "tree-sitter-ruby", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-ruby", - "state" : { - "revision" : "71bd32fb7607035768799732addba884a37a6210", - "version" : "0.23.1" - } - }, - { - "identity" : "tree-sitter-rust", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-rust", - "state" : { - "revision" : "77a3747266f4d621d0757825e6b11edcbf991ca5", - "version" : "0.24.2" - } - }, - { - "identity" : "tree-sitter-scala", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-scala", - "state" : { - "revision" : "38950b525c9dfc44c8b60d44bdd6e54217286ca8", - "version" : "0.26.0" - } - }, - { - "identity" : "tree-sitter-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/alex-pinkus/tree-sitter-swift", - "state" : { - "revision" : "31d17fe7e818a2048c808b5c6fdc2dc792f4f5b5", - "version" : "0.7.3-with-generated-files" - } - }, - { - "identity" : "tree-sitter-toml", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter-grammars/tree-sitter-toml", - "state" : { - "revision" : "64b56832c2cffe41758f28e05c756a3a98d16f41", - "version" : "0.7.0" - } - }, - { - "identity" : "tree-sitter-typescript", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter-typescript", - "state" : { - "revision" : "f975a621f4e7f532fe322e13c4f79495e0a7b2e7", - "version" : "0.23.2" - } - }, - { - "identity" : "tree-sitter-yaml", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter-grammars/tree-sitter-yaml", - "state" : { - "revision" : "b733d3f5f5005890f324333dd57e1f0badec5c87", - "version" : "0.7.0" - } - }, - { - "identity" : "viewinspector", - "kind" : "remoteSourceControl", - "location" : "https://github.com/nalexn/ViewInspector", - "state" : { - "revision" : "e9a06346499a3a889165647e3f23f8a7b2609a1c", - "version" : "0.10.3" - } - }, - { - "identity" : "waterfallgrid", - "kind" : "remoteSourceControl", - "location" : "https://github.com/paololeonardi/WaterfallGrid", - "state" : { - "revision" : "c7c08652c3540adf8e48409c351879b4caea7e89", - "version" : "1.1.0" + "identity": "waterfallgrid", + "kind": "remoteSourceControl", + "location": "https://github.com/paololeonardi/WaterfallGrid", + "state": { + "revision": "c7c08652c3540adf8e48409c351879b4caea7e89", + "version": "1.1.0" } } ], - "version" : 3 + "version": 3 }