file.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import { File } from "@/file"
  2. import { Ripgrep } from "@/file/ripgrep"
  3. import * as InstanceState from "@/effect/instance-state"
  4. import { LSP } from "@/lsp"
  5. import { Effect, Layer, Schema } from "effect"
  6. import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
  7. import { Authorization } from "./auth"
  8. const FileQuery = Schema.Struct({
  9. path: Schema.String,
  10. })
  11. const FindTextQuery = Schema.Struct({
  12. pattern: Schema.String,
  13. })
  14. const FindFileQuery = Schema.Struct({
  15. query: Schema.String,
  16. dirs: Schema.optional(Schema.Literals(["true", "false"])),
  17. type: Schema.optional(Schema.Literals(["file", "directory"])),
  18. limit: Schema.optional(
  19. Schema.NumberFromString.check(
  20. Schema.isInt(),
  21. Schema.isGreaterThanOrEqualTo(1),
  22. Schema.isLessThanOrEqualTo(200),
  23. ),
  24. ),
  25. })
  26. const FindSymbolQuery = Schema.Struct({
  27. query: Schema.String,
  28. })
  29. export const FilePaths = {
  30. findText: "/find",
  31. findFile: "/find/file",
  32. findSymbol: "/find/symbol",
  33. list: "/file",
  34. content: "/file/content",
  35. status: "/file/status",
  36. } as const
  37. export const FileApi = HttpApi.make("file")
  38. .add(
  39. HttpApiGroup.make("file")
  40. .add(
  41. HttpApiEndpoint.get("findText", FilePaths.findText, {
  42. query: FindTextQuery,
  43. success: Schema.Array(Ripgrep.SearchMatch),
  44. }).annotateMerge(
  45. OpenApi.annotations({
  46. identifier: "find.text",
  47. summary: "Find text",
  48. description: "Search for text patterns across files in the project using ripgrep.",
  49. }),
  50. ),
  51. HttpApiEndpoint.get("findFile", FilePaths.findFile, {
  52. query: FindFileQuery,
  53. success: Schema.Array(Schema.String),
  54. }).annotateMerge(
  55. OpenApi.annotations({
  56. identifier: "find.files",
  57. summary: "Find files",
  58. description: "Search for files or directories by name or pattern in the project directory.",
  59. }),
  60. ),
  61. HttpApiEndpoint.get("findSymbol", FilePaths.findSymbol, {
  62. query: FindSymbolQuery,
  63. success: Schema.Array(LSP.Symbol),
  64. }).annotateMerge(
  65. OpenApi.annotations({
  66. identifier: "find.symbols",
  67. summary: "Find symbols",
  68. description: "Search for workspace symbols like functions, classes, and variables using LSP.",
  69. }),
  70. ),
  71. HttpApiEndpoint.get("list", FilePaths.list, {
  72. query: FileQuery,
  73. success: Schema.Array(File.Node),
  74. }).annotateMerge(
  75. OpenApi.annotations({
  76. identifier: "file.list",
  77. summary: "List files",
  78. description: "List files and directories in a specified path.",
  79. }),
  80. ),
  81. HttpApiEndpoint.get("content", FilePaths.content, {
  82. query: FileQuery,
  83. success: File.Content,
  84. }).annotateMerge(
  85. OpenApi.annotations({
  86. identifier: "file.read",
  87. summary: "Read file",
  88. description: "Read the content of a specified file.",
  89. }),
  90. ),
  91. HttpApiEndpoint.get("status", FilePaths.status, {
  92. success: Schema.Array(File.Info),
  93. }).annotateMerge(
  94. OpenApi.annotations({
  95. identifier: "file.status",
  96. summary: "Get file status",
  97. description: "Get the git status of all files in the project.",
  98. }),
  99. ),
  100. )
  101. .annotateMerge(
  102. OpenApi.annotations({
  103. title: "file",
  104. description: "Experimental HttpApi file routes.",
  105. }),
  106. )
  107. .middleware(Authorization),
  108. )
  109. .annotateMerge(
  110. OpenApi.annotations({
  111. title: "opencode experimental HttpApi",
  112. version: "0.0.1",
  113. description: "Experimental HttpApi surface for selected instance routes.",
  114. }),
  115. )
  116. export const fileHandlers = Layer.unwrap(
  117. Effect.gen(function* () {
  118. const svc = yield* File.Service
  119. const ripgrep = yield* Ripgrep.Service
  120. const findText = Effect.fn("FileHttpApi.findText")(function* (ctx: { query: { pattern: string } }) {
  121. return (yield* ripgrep
  122. .search({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 })
  123. .pipe(Effect.orDie)).items
  124. })
  125. const findFile = Effect.fn("FileHttpApi.findFile")(function* (ctx: {
  126. query: { query: string; dirs?: "true" | "false"; type?: "file" | "directory"; limit?: number }
  127. }) {
  128. return yield* svc.search({
  129. query: ctx.query.query,
  130. limit: ctx.query.limit ?? 10,
  131. dirs: ctx.query.dirs !== "false",
  132. type: ctx.query.type,
  133. })
  134. })
  135. const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () {
  136. return []
  137. })
  138. const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) {
  139. return yield* svc.list(ctx.query.path)
  140. })
  141. const content = Effect.fn("FileHttpApi.content")(function* (ctx: { query: { path: string } }) {
  142. return yield* svc.read(ctx.query.path)
  143. })
  144. const status = Effect.fn("FileHttpApi.status")(function* () {
  145. return yield* svc.status()
  146. })
  147. return HttpApiBuilder.group(FileApi, "file", (handlers) =>
  148. handlers
  149. .handle("findText", findText)
  150. .handle("findFile", findFile)
  151. .handle("findSymbol", findSymbol)
  152. .handle("list", list)
  153. .handle("content", content)
  154. .handle("status", status),
  155. )
  156. }),
  157. ).pipe(Layer.provide(File.defaultLayer), Layer.provide(Ripgrep.defaultLayer))