A lexicon-driven AppView for ATProto. happyview.dev
backfill firehose jetstream atproto appview oauth lexicon
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: additional Lua completions and docs

Trezy 05f79fb5 700e9442

+503 -47
+5 -23
web/package-lock.json
··· 110 110 "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", 111 111 "dev": true, 112 112 "license": "MIT", 113 - "peer": true, 114 113 "dependencies": { 115 114 "@babel/code-frame": "^7.29.0", 116 115 "@babel/generator": "^7.29.0", ··· 676 675 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 677 676 "dev": true, 678 677 "license": "MIT", 679 - "peer": true, 680 678 "engines": { 681 679 "node": ">=12" 682 680 }, ··· 1947 1945 "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", 1948 1946 "dev": true, 1949 1947 "license": "MIT", 1950 - "peer": true, 1951 1948 "engines": { 1952 1949 "node": "^14.21.3 || >=16" 1953 1950 }, ··· 4220 4217 "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", 4221 4218 "dev": true, 4222 4219 "license": "MIT", 4223 - "peer": true, 4224 4220 "dependencies": { 4225 4221 "undici-types": "~7.16.0" 4226 4222 } ··· 4231 4227 "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", 4232 4228 "devOptional": true, 4233 4229 "license": "MIT", 4234 - "peer": true, 4235 4230 "dependencies": { 4236 4231 "csstype": "^3.2.2" 4237 4232 } ··· 4242 4237 "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", 4243 4238 "devOptional": true, 4244 4239 "license": "MIT", 4245 - "peer": true, 4246 4240 "peerDependencies": { 4247 4241 "@types/react": "^19.2.0" 4248 4242 } ··· 4259 4253 "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", 4260 4254 "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", 4261 4255 "license": "MIT", 4262 - "optional": true 4256 + "optional": true, 4257 + "peer": true 4263 4258 }, 4264 4259 "node_modules/@types/unist": { 4265 4260 "version": "3.0.3", ··· 4325 4320 "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", 4326 4321 "dev": true, 4327 4322 "license": "MIT", 4328 - "peer": true, 4329 4323 "dependencies": { 4330 4324 "@typescript-eslint/scope-manager": "8.56.0", 4331 4325 "@typescript-eslint/types": "8.56.0", ··· 4852 4846 "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", 4853 4847 "dev": true, 4854 4848 "license": "MIT", 4855 - "peer": true, 4856 4849 "bin": { 4857 4850 "acorn": "bin/acorn" 4858 4851 }, ··· 5239 5232 "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", 5240 5233 "devOptional": true, 5241 5234 "license": "MIT", 5242 - "peer": true, 5243 5235 "dependencies": { 5244 5236 "@babel/types": "^7.26.0" 5245 5237 } ··· 5334 5326 } 5335 5327 ], 5336 5328 "license": "MIT", 5337 - "peer": true, 5338 5329 "dependencies": { 5339 5330 "baseline-browser-mapping": "^2.9.0", 5340 5331 "caniuse-lite": "^1.0.30001759", ··· 6298 6289 "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", 6299 6290 "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", 6300 6291 "license": "(MPL-2.0 OR Apache-2.0)", 6292 + "peer": true, 6301 6293 "optionalDependencies": { 6302 6294 "@types/trusted-types": "^2.0.7" 6303 6295 } ··· 6636 6628 "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", 6637 6629 "dev": true, 6638 6630 "license": "MIT", 6639 - "peer": true, 6640 6631 "dependencies": { 6641 6632 "@eslint-community/eslint-utils": "^4.8.0", 6642 6633 "@eslint-community/regexpp": "^4.12.1", ··· 6777 6768 "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", 6778 6769 "dev": true, 6779 6770 "license": "MIT", 6780 - "peer": true, 6781 6771 "dependencies": { 6782 6772 "@rtsao/scc": "^1.1.0", 6783 6773 "array-includes": "^3.1.9", ··· 7246 7236 "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", 7247 7237 "dev": true, 7248 7238 "license": "MIT", 7249 - "peer": true, 7250 7239 "dependencies": { 7251 7240 "accepts": "^2.0.0", 7252 7241 "body-parser": "^2.2.1", ··· 8039 8028 "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", 8040 8029 "dev": true, 8041 8030 "license": "MIT", 8042 - "peer": true, 8043 8031 "engines": { 8044 8032 "node": ">=16.9.0" 8045 8033 } ··· 9459 9447 "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", 9460 9448 "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", 9461 9449 "license": "MIT", 9450 + "peer": true, 9462 9451 "bin": { 9463 9452 "marked": "bin/marked.js" 9464 9453 }, ··· 11355 11344 "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", 11356 11345 "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", 11357 11346 "license": "MIT", 11358 - "peer": true, 11359 11347 "engines": { 11360 11348 "node": ">=0.10.0" 11361 11349 } ··· 11386 11374 "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", 11387 11375 "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", 11388 11376 "license": "MIT", 11389 - "peer": true, 11390 11377 "dependencies": { 11391 11378 "scheduler": "^0.27.0" 11392 11379 }, ··· 11406 11393 "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", 11407 11394 "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", 11408 11395 "license": "MIT", 11409 - "peer": true, 11410 11396 "dependencies": { 11411 11397 "@types/use-sync-external-store": "^0.0.6", 11412 11398 "use-sync-external-store": "^1.4.0" ··· 11545 11531 "version": "5.0.1", 11546 11532 "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", 11547 11533 "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", 11548 - "license": "MIT", 11549 - "peer": true 11534 + "license": "MIT" 11550 11535 }, 11551 11536 "node_modules/redux-thunk": { 11552 11537 "version": "3.1.0", ··· 12754 12739 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 12755 12740 "dev": true, 12756 12741 "license": "MIT", 12757 - "peer": true, 12758 12742 "engines": { 12759 12743 "node": ">=12" 12760 12744 }, ··· 13022 13006 "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 13023 13007 "dev": true, 13024 13008 "license": "Apache-2.0", 13025 - "peer": true, 13026 13009 "bin": { 13027 13010 "tsc": "bin/tsc", 13028 13011 "tsserver": "bin/tsserver" ··· 13754 13737 "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", 13755 13738 "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", 13756 13739 "license": "MIT", 13757 - "peer": true, 13758 13740 "funding": { 13759 13741 "url": "https://github.com/sponsors/colinhacks" 13760 13742 }
+237 -17
web/src/components/monaco-editor.tsx
··· 12 12 } from "@/lib/lua-completions"; 13 13 import { lexiconJsonSchema, LEXICON_SCHEMA_URI } from "@/lib/lexicon-schema"; 14 14 import { resolveCssColor } from "@/lib/css-utils"; 15 - import { parseLuaIdentifiers, parseRecordVariables } from "@/lib/lua-parser"; 15 + import { parseLuaIdentifiers, parseRecordVariables, parseDbQueryVariables, parseDbQueryRecordIterators } from "@/lib/lua-parser"; 16 + import { HOVER_DOCS } from "@/lib/lua-hover"; 16 17 17 18 const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false }); 18 19 ··· 172 173 }), 173 174 ); 174 175 176 + // Provider 3: Hover documentation 177 + disposablesRef.current.push( 178 + monaco.languages.registerHoverProvider("lua", { 179 + provideHover( 180 + model: editor.ITextModel, 181 + position: Position, 182 + ) { 183 + const word = model.getWordAtPosition(position); 184 + if (!word) return null; 185 + 186 + const lineContent = model.getLineContent(position.lineNumber); 187 + const charBefore = lineContent[word.startColumn - 2]; 188 + let key = word.word; 189 + 190 + if (charBefore === "." || charBefore === ":") { 191 + // Find the module/object prefix before the dot/colon 192 + const textBefore = lineContent.substring(0, word.startColumn - 2); 193 + const prefixMatch = textBefore.match(/(\w+)$/); 194 + if (prefixMatch) { 195 + const sep = charBefore === ":" ? ":" : "."; 196 + key = `${prefixMatch[1]}${sep}${word.word}`; 197 + } 198 + } 199 + 200 + const entry = HOVER_DOCS.get(key); 201 + if (!entry) return null; 202 + 203 + const range = { 204 + startLineNumber: position.lineNumber, 205 + endLineNumber: position.lineNumber, 206 + startColumn: word.startColumn, 207 + endColumn: word.endColumn, 208 + }; 209 + 210 + return { 211 + range, 212 + contents: [ 213 + { value: `\`\`\`lua\n${entry.signature}\n\`\`\`` }, 214 + { value: entry.description }, 215 + ], 216 + }; 217 + }, 218 + }), 219 + ); 220 + 221 + // Provider 4: Signature help 222 + disposablesRef.current.push( 223 + monaco.languages.registerSignatureHelpProvider("lua", { 224 + signatureHelpTriggerCharacters: ["(", ","], 225 + provideSignatureHelp( 226 + model: editor.ITextModel, 227 + position: Position, 228 + ) { 229 + const lineContent = model.getLineContent(position.lineNumber); 230 + const textBeforeCursor = lineContent.substring(0, position.column - 1); 231 + 232 + // Walk backward to find the opening ( and the function name 233 + let depth = 0; 234 + let parenPos = -1; 235 + let activeParam = 0; 236 + for (let i = textBeforeCursor.length - 1; i >= 0; i--) { 237 + const ch = textBeforeCursor[i]; 238 + if (ch === ")") depth++; 239 + else if (ch === "(") { 240 + if (depth === 0) { 241 + parenPos = i; 242 + break; 243 + } 244 + depth--; 245 + } else if (ch === "," && depth === 0) { 246 + activeParam++; 247 + } 248 + } 249 + 250 + if (parenPos < 0) return null; 251 + 252 + // Extract the function name before the ( 253 + const textBeforeParen = textBeforeCursor.substring(0, parenPos); 254 + const fnMatch = textBeforeParen.match(/([\w.]+[:.]\w+|\w+)\s*$/); 255 + if (!fnMatch) return null; 256 + 257 + const fnName = fnMatch[1]; 258 + // Normalize colon to look up both Record:save and Record.save 259 + const entry = HOVER_DOCS.get(fnName) ?? HOVER_DOCS.get(fnName.replace(":", ".")); 260 + if (!entry) return null; 261 + 262 + // Parse parameters from the signature: extract content inside parens 263 + const sigParenMatch = entry.signature.match(/\(([^)]*)\)/); 264 + if (!sigParenMatch) return null; 265 + 266 + const paramString = sigParenMatch[1].trim(); 267 + if (!paramString) return null; 268 + 269 + // Split parameters on commas, respecting brackets 270 + const params: string[] = []; 271 + let current = ""; 272 + let bracketDepth = 0; 273 + for (const ch of paramString) { 274 + if (ch === "[") bracketDepth++; 275 + else if (ch === "]") bracketDepth--; 276 + else if (ch === "," && bracketDepth === 0) { 277 + params.push(current.trim()); 278 + current = ""; 279 + continue; 280 + } 281 + current += ch; 282 + } 283 + if (current.trim()) params.push(current.trim()); 284 + 285 + return { 286 + value: { 287 + signatures: [ 288 + { 289 + label: entry.signature, 290 + documentation: entry.description, 291 + parameters: params.map((p) => ({ 292 + label: p.replace(/^\[?\s*/, "").replace(/\s*\]?\s*$/, ""), 293 + })), 294 + }, 295 + ], 296 + activeSignature: 0, 297 + activeParameter: Math.min(activeParam, params.length - 1), 298 + }, 299 + dispose() {}, 300 + }; 301 + }, 302 + }), 303 + ); 304 + 175 305 // Provider 2: HappyView-specific completions (Record, db, collections) 176 306 if (!completions) return; 177 307 disposablesRef.current.push( ··· 187 317 position.column - 1, 188 318 ); 189 319 320 + // Inside db.query({ ... }) — suggest option keys 321 + if (/db\.query\(\s*\{[^}]*$/.test(textBeforeCursor)) { 322 + const optionEntries = 323 + completionsRef.current?.["db.query"] ?? []; 324 + if (optionEntries.length) { 325 + // Check for collection = " inside db.query — offer NSID completions 326 + const collectionQuoteMatch = textBeforeCursor.match( 327 + /collection\s*=\s*"([^"]*)$/, 328 + ); 329 + if (collectionQuoteMatch) { 330 + const cols = collectionsRef.current; 331 + if (!cols?.length) return { suggestions: [] }; 332 + const quoteCol = textBeforeCursor.lastIndexOf('"'); 333 + const range = { 334 + startLineNumber: position.lineNumber, 335 + endLineNumber: position.lineNumber, 336 + startColumn: quoteCol + 2, 337 + endColumn: position.column, 338 + }; 339 + return { 340 + suggestions: cols.map((col) => ({ 341 + label: col, 342 + kind: monaco.languages.CompletionItemKind.Value, 343 + insertText: col, 344 + range, 345 + })), 346 + }; 347 + } 348 + 349 + const word = model.getWordUntilPosition(position); 350 + const range = { 351 + startLineNumber: position.lineNumber, 352 + endLineNumber: position.lineNumber, 353 + startColumn: word.startColumn, 354 + endColumn: word.endColumn, 355 + }; 356 + return { 357 + suggestions: optionEntries.map((entry) => ({ 358 + label: entry.label, 359 + kind: monaco.languages.CompletionItemKind.Property, 360 + detail: entry.detail, 361 + documentation: entry.description, 362 + insertText: entry.label, 363 + range, 364 + })), 365 + }; 366 + } 367 + } 368 + 369 + // Collection NSID completions for collection = " (outside db.query) and db.count 370 + const collectionAssignMatch = textBeforeCursor.match( 371 + /(?:db\.count\(\s*"([^"]*)$|collection\s*=\s*"([^"]*)$)/, 372 + ); 373 + 190 374 // Record("...") collection completions 191 375 const recordMatch = 192 376 textBeforeCursor.match(/Record\(\s*"([^"]*)$/); 193 - if (recordMatch) { 377 + if (recordMatch || collectionAssignMatch) { 194 378 const cols = collectionsRef.current; 195 379 if (!cols?.length) return { suggestions: [] }; 196 380 const quoteCol = textBeforeCursor.lastIndexOf('"'); ··· 223 407 let entries = completionsRef.current?.[prefix]; 224 408 225 409 if (!entries && prefix !== "Record" && prefix !== "db") { 226 - // Variable access — check if it's a Record variable 227 410 const fullSource = model.getValue(); 228 - const varMap = parseRecordVariables(fullSource); 229 - const collection = varMap[prefix]; 411 + 412 + // Check if it's a db.query() result variable 413 + const dbQueryVars = parseDbQueryVariables(fullSource); 414 + if (prefix in dbQueryVars) { 415 + entries = 416 + completionsRef.current?.["db.query_result"] ?? []; 417 + } 230 418 231 - if (collection) { 232 - // Record instance methods/fields 233 - const instanceEntries = 234 - completionsRef.current?.["Record"]?.filter( 235 - (e) => 236 - e.detail === "method" || e.detail?.endsWith("?"), 237 - ) ?? []; 419 + // Check if it's an iterator over db.query().records 420 + if (!entries) { 421 + const iterMap = parseDbQueryRecordIterators( 422 + fullSource, 423 + dbQueryVars, 424 + ); 425 + const iterCollection = iterMap[prefix]; 426 + if (iterCollection) { 427 + const schemaEntries = 428 + completionsRef.current?.[iterCollection] ?? []; 429 + const uriEntry = { 430 + label: "uri", 431 + detail: "string", 432 + description: "AT URI of the record", 433 + }; 434 + entries = [uriEntry, ...schemaEntries]; 435 + } 436 + } 437 + 438 + if (!entries) { 439 + // Variable access — check if it's a Record variable 440 + const varMap = parseRecordVariables(fullSource); 441 + const collection = varMap[prefix]; 442 + 443 + if (collection) { 444 + // Record instance methods/fields 445 + const instanceEntries = 446 + completionsRef.current?.["Record"]?.filter( 447 + (e) => 448 + e.detail === "method" || e.detail?.endsWith("?"), 449 + ) ?? []; 238 450 239 - // Collection-specific record properties (merged into completions by parent) 240 - const schemaEntries = 241 - completionsRef.current?.[collection] ?? []; 451 + // Collection-specific record properties (merged into completions by parent) 452 + const schemaEntries = 453 + completionsRef.current?.[collection] ?? []; 242 454 243 - entries = [...instanceEntries, ...schemaEntries]; 455 + entries = [...instanceEntries, ...schemaEntries]; 456 + } 244 457 } 245 458 } 246 459 ··· 274 487 : monaco.languages.CompletionItemKind.Field, 275 488 detail: entry.detail, 276 489 documentation: entry.description, 277 - insertText: entry.label, 490 + insertText: entry.insertText ?? entry.label, 491 + ...(entry.insertText 492 + ? { 493 + insertTextRules: 494 + monaco.languages.CompletionItemInsertTextRule 495 + .InsertAsSnippet, 496 + } 497 + : {}), 278 498 range, 279 499 })), 280 500 };
+2 -2
web/src/components/ui/chart.tsx
··· 118 118 color, 119 119 nameKey, 120 120 labelKey, 121 - }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> & 121 + }: RechartsPrimitive.TooltipContentProps<string, string> & 122 122 React.ComponentProps<"div"> & { 123 123 hideLabel?: boolean 124 124 hideIndicator?: boolean ··· 259 259 verticalAlign = "bottom", 260 260 nameKey, 261 261 }: React.ComponentProps<"div"> & 262 - Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { 262 + Pick<RechartsPrimitive.DefaultLegendContentProps, "payload" | "verticalAlign"> & { 263 263 hideIcon?: boolean 264 264 nameKey?: string 265 265 }) {
+72 -5
web/src/lib/lua-completions.ts
··· 2 2 label: string; 3 3 detail?: string; 4 4 description?: string; 5 + insertText?: string; 5 6 } 6 7 7 8 export interface LuaSnippetEntry { ··· 12 13 } 13 14 14 15 export const LUA_KEYWORDS = [ 15 - "and", "break", "do", "else", "end", "false", 16 - "in", "nil", "not", "or", "then", "true", "until", 16 + "and", "break", "do", "else", "elseif", "end", "false", 17 + "for", "function", "goto", "if", "in", "local", "nil", 18 + "not", "or", "repeat", "return", "then", "true", "until", "while", 17 19 ]; 18 20 19 21 export const LUA_BUILTINS = [ 20 22 "print", "tostring", "tonumber", "type", "pairs", "ipairs", "next", 21 23 "select", "unpack", "error", "pcall", "xpcall", "assert", 22 24 "setmetatable", "getmetatable", "rawget", "rawset", "rawequal", 25 + // Standard library modules 26 + "string", "table", "math", "coroutine", "utf8", 23 27 // HappyView sandbox globals 24 28 "input", "params", "caller_did", "collection", "method", 25 29 "now", "log", "TID", ··· 114 118 { label: "_rkey", detail: "string?", description: "Record key (set via set_rkey or generate_rkey)" }, 115 119 ], 116 120 db: [ 117 - { label: "query", detail: "function", description: "Execute a SQL query and return rows" }, 118 - { label: "get", detail: "function", description: "Execute a SQL query and return a single row" }, 119 - { label: "count", detail: "function", description: "Execute a count query" }, 121 + { 122 + label: "query", 123 + detail: "function", 124 + description: "Query records — db.query({ collection, did?, limit?, offset? }) → { records, cursor? }", 125 + insertText: "query({\n\tcollection = ${1:collection},\n})", 126 + }, 127 + { 128 + label: "get", 129 + detail: "function", 130 + description: "Get a single record by AT URI — db.get(uri) → record or nil", 131 + insertText: "get(${1:uri})", 132 + }, 133 + { 134 + label: "count", 135 + detail: "function", 136 + description: "Count records — db.count(collection, did?) → integer", 137 + insertText: "count(${1:collection})", 138 + }, 139 + ], 140 + "db.query": [ 141 + { label: "collection", detail: "string", description: "Collection NSID (required)" }, 142 + { label: "did", detail: "string?", description: "Filter records by DID" }, 143 + { label: "limit", detail: "integer?", description: "Max records to return (max 100, default 20)" }, 144 + { label: "offset", detail: "integer?", description: "Pagination offset (default 0)" }, 145 + ], 146 + "db.query_result": [ 147 + { label: "records", detail: "table[]", description: "Array of record tables (each includes uri)" }, 148 + { label: "cursor", detail: "string?", description: "Pagination cursor (present when more results exist)" }, 120 149 ], 121 150 // Lua standard library modules 122 151 string: [ ··· 137 166 table: [ 138 167 { label: "concat", detail: "function", description: "Concatenate table elements — table.concat(list [, sep [, i [, j]]])" }, 139 168 { label: "insert", detail: "function", description: "Insert element — table.insert(list, [pos,] value)" }, 169 + { label: "move", detail: "function", description: "Move elements between tables — table.move(a1, f, e, t [, a2])" }, 170 + { label: "pack", detail: "function", description: "Pack arguments into table with n field — table.pack(···)" }, 140 171 { label: "remove", detail: "function", description: "Remove element — table.remove(list [, pos])" }, 141 172 { label: "sort", detail: "function", description: "Sort table in-place — table.sort(list [, comp])" }, 142 173 { label: "unpack", detail: "function", description: "Unpack table elements — table.unpack(list [, i [, j]])" }, 143 174 ], 144 175 math: [ 145 176 { label: "abs", detail: "function", description: "Absolute value — math.abs(x)" }, 177 + { label: "acos", detail: "function", description: "Arc cosine — math.acos(x)" }, 178 + { label: "asin", detail: "function", description: "Arc sine — math.asin(x)" }, 179 + { label: "atan", detail: "function", description: "Arc tangent — math.atan(y [, x])" }, 146 180 { label: "ceil", detail: "function", description: "Round up — math.ceil(x)" }, 181 + { label: "cos", detail: "function", description: "Cosine — math.cos(x)" }, 182 + { label: "deg", detail: "function", description: "Radians to degrees — math.deg(x)" }, 183 + { label: "exp", detail: "function", description: "e^x — math.exp(x)" }, 147 184 { label: "floor", detail: "function", description: "Round down — math.floor(x)" }, 185 + { label: "fmod", detail: "function", description: "Remainder — math.fmod(x, y)" }, 186 + { label: "log", detail: "function", description: "Logarithm — math.log(x [, base])" }, 148 187 { label: "max", detail: "function", description: "Maximum value — math.max(x, ···)" }, 188 + { label: "maxinteger", detail: "number", description: "Maximum integer value" }, 149 189 { label: "min", detail: "function", description: "Minimum value — math.min(x, ···)" }, 190 + { label: "mininteger", detail: "number", description: "Minimum integer value" }, 191 + { label: "modf", detail: "function", description: "Integer and fractional parts — math.modf(x)" }, 192 + { label: "rad", detail: "function", description: "Degrees to radians — math.rad(x)" }, 150 193 { label: "random", detail: "function", description: "Generate random number — math.random([m [, n]])" }, 194 + { label: "randomseed", detail: "function", description: "Set random seed — math.randomseed([x [, y]])" }, 195 + { label: "sin", detail: "function", description: "Sine — math.sin(x)" }, 151 196 { label: "sqrt", detail: "function", description: "Square root — math.sqrt(x)" }, 197 + { label: "tan", detail: "function", description: "Tangent — math.tan(x)" }, 198 + { label: "tointeger", detail: "function", description: "Convert to integer or nil — math.tointeger(x)" }, 199 + { label: "type", detail: "function", description: "Number type (\"integer\", \"float\", or false) — math.type(x)" }, 200 + { label: "ult", detail: "function", description: "Unsigned integer comparison — math.ult(m, n)" }, 152 201 { label: "huge", detail: "number", description: "Infinity value" }, 153 202 { label: "pi", detail: "number", description: "Pi constant (3.14159...)" }, 203 + ], 204 + coroutine: [ 205 + { label: "create", detail: "function", description: "Create a coroutine — coroutine.create(f)" }, 206 + { label: "resume", detail: "function", description: "Resume a coroutine — coroutine.resume(co [, val1, ···])" }, 207 + { label: "yield", detail: "function", description: "Suspend coroutine — coroutine.yield(···)" }, 208 + { label: "status", detail: "function", description: "Coroutine status — coroutine.status(co)" }, 209 + { label: "wrap", detail: "function", description: "Create iterator from coroutine — coroutine.wrap(f)" }, 210 + { label: "isyieldable", detail: "function", description: "Check if running coroutine can yield — coroutine.isyieldable()" }, 211 + { label: "running", detail: "function", description: "Returns running coroutine — coroutine.running()" }, 212 + { label: "close", detail: "function", description: "Close a coroutine — coroutine.close(co)" }, 213 + ], 214 + utf8: [ 215 + { label: "char", detail: "function", description: "UTF-8 string from codepoints — utf8.char(···)" }, 216 + { label: "charpattern", detail: "string", description: "Pattern matching one UTF-8 character" }, 217 + { label: "codepoint", detail: "function", description: "Codepoints from string — utf8.codepoint(s [, i [, j [, lax]]])" }, 218 + { label: "codes", detail: "function", description: "Iterator over UTF-8 codepoints — utf8.codes(s [, lax])" }, 219 + { label: "len", detail: "function", description: "UTF-8 string length — utf8.len(s [, i [, j [, lax]]])" }, 220 + { label: "offset", detail: "function", description: "Byte offset of nth character — utf8.offset(s, n [, i])" }, 154 221 ], 155 222 }; 156 223
+148
web/src/lib/lua-hover.ts
··· 1 + export interface HoverEntry { 2 + signature: string; 3 + description: string; 4 + module?: string; 5 + } 6 + 7 + export const HOVER_DOCS = new Map<string, HoverEntry>([ 8 + // ── Lua keywords ────────────────────────────────────────────────────── 9 + ["and", { signature: "and", description: "Logical AND operator — returns first falsy operand or last operand" }], 10 + ["break", { signature: "break", description: "Exit the innermost loop" }], 11 + ["do", { signature: "do ... end", description: "Block delimiter — creates a new scope" }], 12 + ["else", { signature: "else", description: "Alternative branch in an if statement" }], 13 + ["elseif", { signature: "elseif condition then", description: "Additional condition branch in an if statement" }], 14 + ["end", { signature: "end", description: "Closes a block (function, if, for, while, do)" }], 15 + ["false", { signature: "false", description: "Boolean false value" }], 16 + ["for", { signature: "for var = start, stop [, step] do ... end", description: "Numeric or generic loop" }], 17 + ["function", { signature: "function name(...) ... end", description: "Define a function" }], 18 + ["goto", { signature: "goto label", description: "Jump to a label within the same block" }], 19 + ["if", { signature: "if condition then ... end", description: "Conditional branch" }], 20 + ["in", { signature: "in", description: "Iterator separator in generic for loops" }], 21 + ["local", { signature: "local name [= value]", description: "Declare a local variable" }], 22 + ["nil", { signature: "nil", description: "The nil value — represents absence of a value" }], 23 + ["not", { signature: "not", description: "Logical NOT operator" }], 24 + ["or", { signature: "or", description: "Logical OR operator — returns first truthy operand or last operand" }], 25 + ["repeat", { signature: "repeat ... until condition", description: "Loop that runs at least once" }], 26 + ["return", { signature: "return [values]", description: "Return values from a function" }], 27 + ["then", { signature: "then", description: "Begins the body of an if/elseif branch" }], 28 + ["true", { signature: "true", description: "Boolean true value" }], 29 + ["until", { signature: "until condition", description: "End condition of a repeat loop" }], 30 + ["while", { signature: "while condition do ... end", description: "Loop while condition is true" }], 31 + 32 + // ── Global builtins ─────────────────────────────────────────────────── 33 + ["print", { signature: "print(···)", description: "Print values to stdout (separated by tabs)" }], 34 + ["tostring", { signature: "tostring(v)", description: "Convert any value to a string" }], 35 + ["tonumber", { signature: "tonumber(e [, base])", description: "Convert a value to a number, or nil if not convertible" }], 36 + ["type", { signature: "type(v)", description: "Returns the type of a value as a string" }], 37 + ["pairs", { signature: "pairs(t)", description: "Returns an iterator over all key-value pairs in a table" }], 38 + ["ipairs", { signature: "ipairs(t)", description: "Returns an iterator over integer-keyed elements (1, 2, ...)" }], 39 + ["next", { signature: "next(table [, index])", description: "Returns the next key-value pair after the given key" }], 40 + ["select", { signature: "select(index, ···)", description: "Returns arguments after the given index, or count with '#'" }], 41 + ["unpack", { signature: "unpack(list [, i [, j]])", description: "Returns elements from a table (alias for table.unpack)" }], 42 + ["error", { signature: "error(message [, level])", description: "Raise an error with a message" }], 43 + ["pcall", { signature: "pcall(f [, arg1, ···])", description: "Call a function in protected mode — returns ok, result" }], 44 + ["xpcall", { signature: "xpcall(f, msgh [, arg1, ···])", description: "Like pcall but with a custom error handler" }], 45 + ["assert", { signature: "assert(v [, message])", description: "Raise an error if v is falsy" }], 46 + ["setmetatable", { signature: "setmetatable(table, metatable)", description: "Set the metatable for a table" }], 47 + ["getmetatable", { signature: "getmetatable(object)", description: "Returns the metatable of an object, or nil" }], 48 + ["rawget", { signature: "rawget(table, index)", description: "Get a value without invoking __index metamethod" }], 49 + ["rawset", { signature: "rawset(table, index, value)", description: "Set a value without invoking __newindex metamethod" }], 50 + ["rawequal", { signature: "rawequal(v1, v2)", description: "Check equality without invoking __eq metamethod" }], 51 + 52 + // ── HappyView sandbox globals ───────────────────────────────────────── 53 + ["input", { signature: "input", description: "Procedure input table (from request body)" }], 54 + ["params", { signature: "params", description: "Query parameters table (from URL query string)" }], 55 + ["caller_did", { signature: "caller_did", description: "DID of the authenticated caller" }], 56 + ["collection", { signature: "collection", description: "Target collection NSID for this lexicon" }], 57 + ["method", { signature: "method", description: "XRPC method name being called" }], 58 + ["now", { signature: "now()", description: "Current UTC timestamp as ISO 8601 string" }], 59 + ["log", { signature: "log(···)", description: "Log values to server console" }], 60 + ["TID", { signature: "TID()", description: "Generate a new TID (timestamp identifier)" }], 61 + 62 + // ── string module ───────────────────────────────────────────────────── 63 + ["string.byte", { signature: "string.byte(s [, i [, j]])", description: "Returns internal numeric codes of characters", module: "string" }], 64 + ["string.char", { signature: "string.char(···)", description: "Returns a string from character codes", module: "string" }], 65 + ["string.find", { signature: "string.find(s, pattern [, init [, plain]])", description: "Find first match of pattern in string", module: "string" }], 66 + ["string.format", { signature: "string.format(formatstring, ···)", description: "Format a string (like sprintf)", module: "string" }], 67 + ["string.gmatch", { signature: "string.gmatch(s, pattern)", description: "Returns an iterator for all pattern matches", module: "string" }], 68 + ["string.gsub", { signature: "string.gsub(s, pattern, repl [, n])", description: "Global substitution — replace pattern matches", module: "string" }], 69 + ["string.len", { signature: "string.len(s)", description: "Returns the byte length of a string", module: "string" }], 70 + ["string.lower", { signature: "string.lower(s)", description: "Returns lowercase copy of a string", module: "string" }], 71 + ["string.match", { signature: "string.match(s, pattern [, init])", description: "Find first match and return captures", module: "string" }], 72 + ["string.rep", { signature: "string.rep(s, n [, sep])", description: "Returns a repeated copy of a string", module: "string" }], 73 + ["string.reverse", { signature: "string.reverse(s)", description: "Returns reversed string", module: "string" }], 74 + ["string.sub", { signature: "string.sub(s, i [, j])", description: "Returns a substring", module: "string" }], 75 + ["string.upper", { signature: "string.upper(s)", description: "Returns uppercase copy of a string", module: "string" }], 76 + 77 + // ── table module ────────────────────────────────────────────────────── 78 + ["table.concat", { signature: "table.concat(list [, sep [, i [, j]]])", description: "Concatenate table elements into a string", module: "table" }], 79 + ["table.insert", { signature: "table.insert(list, [pos,] value)", description: "Insert an element into a table", module: "table" }], 80 + ["table.move", { signature: "table.move(a1, f, e, t [, a2])", description: "Move elements between tables", module: "table" }], 81 + ["table.pack", { signature: "table.pack(···)", description: "Pack arguments into a table with n field", module: "table" }], 82 + ["table.remove", { signature: "table.remove(list [, pos])", description: "Remove an element from a table", module: "table" }], 83 + ["table.sort", { signature: "table.sort(list [, comp])", description: "Sort a table in-place", module: "table" }], 84 + ["table.unpack", { signature: "table.unpack(list [, i [, j]])", description: "Unpack table elements as multiple return values", module: "table" }], 85 + 86 + // ── math module ─────────────────────────────────────────────────────── 87 + ["math.abs", { signature: "math.abs(x)", description: "Absolute value", module: "math" }], 88 + ["math.acos", { signature: "math.acos(x)", description: "Arc cosine (in radians)", module: "math" }], 89 + ["math.asin", { signature: "math.asin(x)", description: "Arc sine (in radians)", module: "math" }], 90 + ["math.atan", { signature: "math.atan(y [, x])", description: "Arc tangent of y/x (in radians)", module: "math" }], 91 + ["math.ceil", { signature: "math.ceil(x)", description: "Round up to nearest integer", module: "math" }], 92 + ["math.cos", { signature: "math.cos(x)", description: "Cosine (x in radians)", module: "math" }], 93 + ["math.deg", { signature: "math.deg(x)", description: "Convert radians to degrees", module: "math" }], 94 + ["math.exp", { signature: "math.exp(x)", description: "Returns e^x", module: "math" }], 95 + ["math.floor", { signature: "math.floor(x)", description: "Round down to nearest integer", module: "math" }], 96 + ["math.fmod", { signature: "math.fmod(x, y)", description: "Remainder of x / y", module: "math" }], 97 + ["math.huge", { signature: "math.huge", description: "Infinity value (HUGE_VAL)", module: "math" }], 98 + ["math.log", { signature: "math.log(x [, base])", description: "Logarithm of x in the given base (default: e)", module: "math" }], 99 + ["math.max", { signature: "math.max(x, ···)", description: "Returns the maximum value among arguments", module: "math" }], 100 + ["math.maxinteger", { signature: "math.maxinteger", description: "Maximum integer value (2^63 - 1)", module: "math" }], 101 + ["math.min", { signature: "math.min(x, ···)", description: "Returns the minimum value among arguments", module: "math" }], 102 + ["math.mininteger", { signature: "math.mininteger", description: "Minimum integer value (-2^63)", module: "math" }], 103 + ["math.modf", { signature: "math.modf(x)", description: "Returns integer and fractional parts of x", module: "math" }], 104 + ["math.pi", { signature: "math.pi", description: "Pi constant (3.14159...)", module: "math" }], 105 + ["math.rad", { signature: "math.rad(x)", description: "Convert degrees to radians", module: "math" }], 106 + ["math.random", { signature: "math.random([m [, n]])", description: "Generate a pseudo-random number", module: "math" }], 107 + ["math.randomseed", { signature: "math.randomseed([x [, y]])", description: "Set the random seed", module: "math" }], 108 + ["math.sin", { signature: "math.sin(x)", description: "Sine (x in radians)", module: "math" }], 109 + ["math.sqrt", { signature: "math.sqrt(x)", description: "Square root of x", module: "math" }], 110 + ["math.tan", { signature: "math.tan(x)", description: "Tangent (x in radians)", module: "math" }], 111 + ["math.tointeger", { signature: "math.tointeger(x)", description: "Convert to integer if possible, otherwise nil", module: "math" }], 112 + ["math.type", { signature: "math.type(x)", description: "Returns \"integer\", \"float\", or false", module: "math" }], 113 + ["math.ult", { signature: "math.ult(m, n)", description: "Unsigned integer less-than comparison", module: "math" }], 114 + 115 + // ── coroutine module ────────────────────────────────────────────────── 116 + ["coroutine.create", { signature: "coroutine.create(f)", description: "Create a new coroutine from function f", module: "coroutine" }], 117 + ["coroutine.resume", { signature: "coroutine.resume(co [, val1, ···])", description: "Resume a suspended coroutine", module: "coroutine" }], 118 + ["coroutine.yield", { signature: "coroutine.yield(···)", description: "Suspend the running coroutine", module: "coroutine" }], 119 + ["coroutine.status", { signature: "coroutine.status(co)", description: "Returns coroutine status: \"running\", \"suspended\", \"normal\", or \"dead\"", module: "coroutine" }], 120 + ["coroutine.wrap", { signature: "coroutine.wrap(f)", description: "Create an iterator function from a coroutine", module: "coroutine" }], 121 + ["coroutine.isyieldable", { signature: "coroutine.isyieldable()", description: "Returns true if the running coroutine can yield", module: "coroutine" }], 122 + ["coroutine.running", { signature: "coroutine.running()", description: "Returns the running coroutine and a boolean (true if main)", module: "coroutine" }], 123 + ["coroutine.close", { signature: "coroutine.close(co)", description: "Close a coroutine and release its resources", module: "coroutine" }], 124 + 125 + // ── utf8 module ─────────────────────────────────────────────────────── 126 + ["utf8.char", { signature: "utf8.char(···)", description: "Returns a UTF-8 string from codepoint values", module: "utf8" }], 127 + ["utf8.charpattern", { signature: "utf8.charpattern", description: "Pattern that matches exactly one UTF-8 character", module: "utf8" }], 128 + ["utf8.codepoint", { signature: "utf8.codepoint(s [, i [, j [, lax]]])", description: "Returns codepoint values of characters in a string", module: "utf8" }], 129 + ["utf8.codes", { signature: "utf8.codes(s [, lax])", description: "Returns an iterator over UTF-8 codepoints", module: "utf8" }], 130 + ["utf8.len", { signature: "utf8.len(s [, i [, j [, lax]]])", description: "Returns the number of UTF-8 characters in a string", module: "utf8" }], 131 + ["utf8.offset", { signature: "utf8.offset(s, n [, i])", description: "Returns the byte position of the nth UTF-8 character", module: "utf8" }], 132 + 133 + // ── HappyView Record API ────────────────────────────────────────────── 134 + ["Record", { signature: "Record(collection)", description: "Create a new record for the given collection NSID" }], 135 + ["Record.load", { signature: "Record.load(uri)", description: "Load a record from the database by AT URI" }], 136 + ["Record.load_all", { signature: "Record.load_all({uri1, uri2, ...})", description: "Load multiple records from the database" }], 137 + ["Record.save_all", { signature: "Record.save_all({r1, r2, ...})", description: "Save multiple records in parallel" }], 138 + ["Record:save", { signature: "record:save()", description: "Save this record (creates or updates on PDS and database)" }], 139 + ["Record:delete", { signature: "record:delete()", description: "Delete this record from PDS and database" }], 140 + ["Record:set_key_type", { signature: "record:set_key_type(type)", description: "Set the record key type (tid, any, nsid, literal:*)" }], 141 + ["Record:set_rkey", { signature: "record:set_rkey(key)", description: "Set a specific rkey for this record" }], 142 + ["Record:generate_rkey", { signature: "record:generate_rkey()", description: "Generate an rkey based on the record's _key_type" }], 143 + 144 + // ── HappyView db API ────────────────────────────────────────────────── 145 + ["db.query", { signature: "db.query({collection, did?, limit?, offset?})", description: "Query records — returns {records, cursor?}", module: "db" }], 146 + ["db.get", { signature: "db.get(uri)", description: "Get a single record by AT URI — returns record or nil", module: "db" }], 147 + ["db.count", { signature: "db.count(collection [, did])", description: "Count records in a collection", module: "db" }], 148 + ]);
+39
web/src/lib/lua-parser.ts
··· 37 37 } 38 38 return map; 39 39 } 40 + 41 + /** Parse Lua source for `db.query({ collection = "..." })` assignments. 42 + * Returns a map of variable name → collection NSID. */ 43 + export function parseDbQueryVariables( 44 + source: string, 45 + ): Record<string, string | null> { 46 + const map: Record<string, string | null> = {}; 47 + const re = /(?:local\s+)?(\w+)\s*=\s*db\.query\s*\(\s*\{([^}]*)\}/g; 48 + let m; 49 + while ((m = re.exec(source)) !== null) { 50 + const varName = m[1]; 51 + const body = m[2]; 52 + const colMatch = body.match(/collection\s*=\s*"([^"]+)"/); 53 + map[varName] = colMatch ? colMatch[1] : null; 54 + } 55 + return map; 56 + } 57 + 58 + /** Parse `for _, var in ipairs(result.records)` loops and resolve the 59 + * iterator variable back to the collection from a db.query result. 60 + * Returns a map of iterator variable name → collection NSID. */ 61 + export function parseDbQueryRecordIterators( 62 + source: string, 63 + queryVars: Record<string, string | null>, 64 + ): Record<string, string> { 65 + const map: Record<string, string> = {}; 66 + const re = 67 + /\bfor\s+(?:\w+\s*,\s*)?(\w+)\s+in\s+i?pairs\s*\(\s*(\w+)\.records\s*\)/g; 68 + let m; 69 + while ((m = re.exec(source)) !== null) { 70 + const iterVar = m[1]; 71 + const resultVar = m[2]; 72 + const collection = queryVars[resultVar]; 73 + if (collection) { 74 + map[iterVar] = collection; 75 + } 76 + } 77 + return map; 78 + }