comments.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import { batch, createMemo, createRoot, createSignal, onCleanup } from "solid-js"
  2. import { createStore } from "solid-js/store"
  3. import { createSimpleContext } from "@opencode-ai/ui/context"
  4. import { useParams } from "@solidjs/router"
  5. import { Persist, persisted } from "@/utils/persist"
  6. import type { SelectedLineRange } from "@/context/file"
  7. export type LineComment = {
  8. id: string
  9. file: string
  10. selection: SelectedLineRange
  11. comment: string
  12. time: number
  13. }
  14. type CommentFocus = { file: string; id: string }
  15. const WORKSPACE_KEY = "__workspace__"
  16. const MAX_COMMENT_SESSIONS = 20
  17. type CommentSession = ReturnType<typeof createCommentSession>
  18. type CommentCacheEntry = {
  19. value: CommentSession
  20. dispose: VoidFunction
  21. }
  22. function createCommentSession(dir: string, id: string | undefined) {
  23. const legacy = `${dir}/comments${id ? "/" + id : ""}.v1`
  24. const [store, setStore, _, ready] = persisted(
  25. Persist.scoped(dir, id, "comments", [legacy]),
  26. createStore<{
  27. comments: Record<string, LineComment[]>
  28. }>({
  29. comments: {},
  30. }),
  31. )
  32. const [focus, setFocus] = createSignal<CommentFocus | null>(null)
  33. const [active, setActive] = createSignal<CommentFocus | null>(null)
  34. const list = (file: string) => store.comments[file] ?? []
  35. const add = (input: Omit<LineComment, "id" | "time">) => {
  36. const next: LineComment = {
  37. id: crypto.randomUUID(),
  38. time: Date.now(),
  39. ...input,
  40. }
  41. batch(() => {
  42. setStore("comments", input.file, (items) => [...(items ?? []), next])
  43. setFocus({ file: input.file, id: next.id })
  44. })
  45. return next
  46. }
  47. const remove = (file: string, id: string) => {
  48. setStore("comments", file, (items) => (items ?? []).filter((x) => x.id !== id))
  49. setFocus((current) => (current?.id === id ? null : current))
  50. }
  51. const all = createMemo(() => {
  52. const files = Object.keys(store.comments)
  53. const items = files.flatMap((file) => store.comments[file] ?? [])
  54. return items.slice().sort((a, b) => a.time - b.time)
  55. })
  56. return {
  57. ready,
  58. list,
  59. all,
  60. add,
  61. remove,
  62. focus: createMemo(() => focus()),
  63. setFocus,
  64. clearFocus: () => setFocus(null),
  65. active: createMemo(() => active()),
  66. setActive,
  67. clearActive: () => setActive(null),
  68. }
  69. }
  70. export const { use: useComments, provider: CommentsProvider } = createSimpleContext({
  71. name: "Comments",
  72. gate: false,
  73. init: () => {
  74. const params = useParams()
  75. const cache = new Map<string, CommentCacheEntry>()
  76. const disposeAll = () => {
  77. for (const entry of cache.values()) {
  78. entry.dispose()
  79. }
  80. cache.clear()
  81. }
  82. onCleanup(disposeAll)
  83. const prune = () => {
  84. while (cache.size > MAX_COMMENT_SESSIONS) {
  85. const first = cache.keys().next().value
  86. if (!first) return
  87. const entry = cache.get(first)
  88. entry?.dispose()
  89. cache.delete(first)
  90. }
  91. }
  92. const load = (dir: string, id: string | undefined) => {
  93. const key = `${dir}:${id ?? WORKSPACE_KEY}`
  94. const existing = cache.get(key)
  95. if (existing) {
  96. cache.delete(key)
  97. cache.set(key, existing)
  98. return existing.value
  99. }
  100. const entry = createRoot((dispose) => ({
  101. value: createCommentSession(dir, id),
  102. dispose,
  103. }))
  104. cache.set(key, entry)
  105. prune()
  106. return entry.value
  107. }
  108. const session = createMemo(() => load(params.dir!, params.id))
  109. return {
  110. ready: () => session().ready(),
  111. list: (file: string) => session().list(file),
  112. all: () => session().all(),
  113. add: (input: Omit<LineComment, "id" | "time">) => session().add(input),
  114. remove: (file: string, id: string) => session().remove(file, id),
  115. focus: () => session().focus(),
  116. setFocus: (focus: CommentFocus | null) => session().setFocus(focus),
  117. clearFocus: () => session().clearFocus(),
  118. active: () => session().active(),
  119. setActive: (active: CommentFocus | null) => session().setActive(active),
  120. clearActive: () => session().clearActive(),
  121. }
  122. },
  123. })