@rejot-dev/thalo 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,10 @@
1
1
  import { entryMatchesQuery } from "../query.js";
2
2
  import { UncommittedChangesError } from "./change-tracker.js";
3
3
  import { extractSourceFile } from "../../ast/extract.js";
4
+ import { parseDocument } from "../../parser.node.js";
4
5
  import { getEntryIdentity, serializeIdentity } from "../../merge/entry-matcher.js";
5
6
  import { entriesEqual } from "../../merge/entry-merger.js";
6
7
  import { commitExists, detectGitContext, getBlameCommitsForLineRange, getBlameIgnoreRevs, getCurrentCommit, getFileAtCommit, getFilesChangedSince, getUncommittedFiles, isCommitAncestorOf } from "../../git/git.js";
7
- import { parseDocument } from "../../parser.js";
8
8
 
9
9
  //#region src/services/change-tracker/git-tracker.ts
10
10
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"git-tracker.js","names":["changedEntries: InstanceEntry[]","results: InstanceEntry[]","changed: InstanceEntry[]","changed"],"sources":["../../../src/services/change-tracker/git-tracker.ts"],"sourcesContent":["import type { Entry, InstanceEntry } from \"../../ast/ast-types.js\";\nimport type { Query } from \"../query.js\";\nimport type { Workspace } from \"../../model/workspace.js\";\nimport {\n detectGitContext,\n getCurrentCommit,\n getFilesChangedSince,\n getFileAtCommit,\n commitExists,\n getUncommittedFiles,\n getBlameIgnoreRevs,\n getBlameCommitsForLineRange,\n isCommitAncestorOf,\n type FileChange,\n} from \"../../git/git.js\";\nimport { getEntryIdentity, serializeIdentity } from \"../../merge/entry-matcher.js\";\nimport { entriesEqual } from \"../../merge/entry-merger.js\";\nimport { entryMatchesQuery } from \"../query.js\";\nimport { parseDocument } from \"../../parser.js\";\nimport { extractSourceFile } from \"../../ast/extract.js\";\nimport type {\n ChangeTracker,\n ChangeMarker,\n ChangedEntriesResult,\n ChangeTrackerOptions,\n} from \"./change-tracker.js\";\nimport { UncommittedChangesError } from \"./change-tracker.js\";\n\n/**\n * Git-based change tracker.\n *\n * Uses git to determine which entries have changed since the last actualization.\n * This allows detection of in-place edits, not just new entries.\n *\n * Algorithm:\n * 1. Get files modified since the marker commit\n * 2. For each modified .thalo file:\n * - Get file content at the marker commit\n * - Parse both versions\n * - Compare entries by identity (linkId or timestamp)\n * - Mark entries as changed if they're new or content differs\n */\nexport class GitChangeTracker implements ChangeTracker {\n readonly type = \"git\" as const;\n private cwd: string;\n private force: boolean;\n private blameIgnoreRevs: string[] | null | undefined;\n\n constructor(options: ChangeTrackerOptions = {}) {\n this.cwd = options.cwd ?? process.cwd();\n this.force = options.force ?? false;\n }\n\n private async getIgnoreRevs(): Promise<string[] | null> {\n if (this.blameIgnoreRevs !== undefined) {\n return this.blameIgnoreRevs;\n }\n this.blameIgnoreRevs = await getBlameIgnoreRevs(this.cwd);\n return this.blameIgnoreRevs;\n }\n\n async getCurrentMarker(): Promise<ChangeMarker> {\n const commit = await getCurrentCommit(this.cwd);\n if (!commit) {\n throw new Error(\"Not in a git repository\");\n }\n return {\n type: \"git\",\n value: commit,\n };\n }\n\n async getChangedEntries(\n workspace: Workspace,\n queries: Query[],\n marker: ChangeMarker | null,\n ): Promise<ChangedEntriesResult> {\n // Validate git context\n const gitContext = await detectGitContext(this.cwd);\n if (!gitContext.isGitRepo) {\n throw new Error(\"Not in a git repository\");\n }\n\n const currentMarker = await this.getCurrentMarker();\n\n // Find files that contain matching entries (source files for the queries)\n const sourceFiles = this.getSourceFiles(workspace, queries);\n\n // Check for uncommitted changes in source files (unless force is set)\n if (!this.force && sourceFiles.length > 0) {\n const uncommitted = await getUncommittedFiles(this.cwd, sourceFiles);\n if (uncommitted.length > 0) {\n throw new UncommittedChangesError(uncommitted);\n }\n }\n\n // If no marker, return all matching entries (first run)\n if (!marker) {\n return {\n entries: this.getAllMatchingEntries(workspace, queries),\n currentMarker,\n };\n }\n\n // If marker is a timestamp, we can still handle it by returning all entries\n // (graceful fallback - user switched from timestamp to git tracking)\n if (marker.type === \"ts\") {\n return {\n entries: this.getAllMatchingEntries(workspace, queries),\n currentMarker,\n };\n }\n\n // Validate the commit exists\n const exists = await commitExists(marker.value, this.cwd);\n if (!exists) {\n // Commit doesn't exist (maybe rebased or squashed)\n // Return all matching entries as fallback\n return {\n entries: this.getAllMatchingEntries(workspace, queries),\n currentMarker,\n };\n }\n\n // Get files changed since the marker commit (with rename detection)\n const changedFiles = await getFilesChangedSince(marker.value, this.cwd);\n const thaloChanges = changedFiles.filter(\n (f) => f.path.endsWith(\".thalo\") || f.path.endsWith(\".md\"),\n );\n\n // If no thalo files changed, return empty\n if (thaloChanges.length === 0) {\n return {\n entries: [],\n currentMarker,\n };\n }\n\n // Find changed entries\n const changedEntries: InstanceEntry[] = [];\n const seenKeys = new Set<string>();\n\n for (const change of thaloChanges) {\n const entries = await this.getChangedEntriesInFile(workspace, change, marker.value, queries);\n\n for (const entry of entries) {\n // Include file path in dedup key to handle timestamp-based identities across files\n // (Link IDs are globally unique, but timestamps can collide across files)\n const key = `${change.path}:${serializeIdentity(getEntryIdentity(entry))}`;\n if (!seenKeys.has(key)) {\n seenKeys.add(key);\n changedEntries.push(entry);\n }\n }\n }\n\n return {\n entries: changedEntries,\n currentMarker,\n };\n }\n\n /**\n * Get all files that contain entries matching the queries.\n */\n private getSourceFiles(workspace: Workspace, queries: Query[]): string[] {\n const files = new Set<string>();\n\n for (const model of workspace.allModels()) {\n for (const entry of model.ast.entries) {\n if (entry.type !== \"instance_entry\") {\n continue;\n }\n\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n files.add(model.file);\n break;\n }\n }\n }\n }\n\n return Array.from(files);\n }\n\n /**\n * Get all entries matching the queries (for first run or fallback)\n */\n private getAllMatchingEntries(workspace: Workspace, queries: Query[]): InstanceEntry[] {\n const results: InstanceEntry[] = [];\n const seen = new Set<string>();\n\n for (const model of workspace.allModels()) {\n for (const entry of model.ast.entries) {\n if (entry.type !== \"instance_entry\") {\n continue;\n }\n\n const key = `${model.file}:${serializeIdentity(getEntryIdentity(entry))}`;\n\n if (seen.has(key)) {\n continue;\n }\n\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n results.push(entry);\n seen.add(key);\n break;\n }\n }\n }\n }\n\n return results;\n }\n\n /**\n * Get changed entries in a specific file.\n *\n * Note: This method only returns entries that exist in the current version of the file.\n * Deleted entries are intentionally not tracked because for the actualization use case,\n * we want entries to include in the synthesis prompt - deleted entries don't exist\n * and therefore don't need to be synthesized.\n */\n private async getChangedEntriesInFile(\n workspace: Workspace,\n change: FileChange,\n markerCommit: string,\n queries: Query[],\n ): Promise<InstanceEntry[]> {\n // Find the model for this file in the workspace (use current path)\n const model = this.findModelByRelativePath(workspace, change.path);\n if (!model) {\n return [];\n }\n\n // Get current entries\n const currentEntries = model.ast.entries.filter(\n (e): e is InstanceEntry => e.type === \"instance_entry\",\n );\n\n // If a blame ignore-revs file exists, use blame-based change detection.\n // This allows users to ignore formatting-only commits (e.g. via `.git-blame-ignore-revs`)\n // so they don't retrigger syntheses.\n const ignoreRevs = await this.getIgnoreRevs();\n if (ignoreRevs) {\n const changed: InstanceEntry[] = [];\n\n // Normalize end line: tree-sitter endPosition is exclusive.\n function locationToLineRange(entry: InstanceEntry): { startLine: number; endLine: number } {\n const startRow = entry.location.startPosition.row;\n const endRow = entry.location.endPosition.row;\n const endCol = entry.location.endPosition.column;\n const startLine = startRow + 1;\n const endLine = endCol === 0 ? endRow : endRow + 1;\n return { startLine, endLine: Math.max(startLine, endLine) };\n }\n\n for (const entry of currentEntries) {\n // Check if entry matches any query\n let matchesQuery = false;\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n matchesQuery = true;\n break;\n }\n }\n if (!matchesQuery) {\n continue;\n }\n\n const { startLine, endLine } = locationToLineRange(entry);\n const blamedCommits = await getBlameCommitsForLineRange(\n change.path,\n startLine,\n endLine,\n this.cwd,\n ignoreRevs,\n );\n\n let isChanged = false;\n for (const commit of blamedCommits) {\n // If the blamed commit is not an ancestor of the marker, then the entry\n // has changes \"after\" the marker (including merges), after applying ignore-revs.\n const isBeforeMarker = await isCommitAncestorOf(commit, markerCommit, this.cwd);\n if (!isBeforeMarker) {\n isChanged = true;\n break;\n }\n }\n\n if (isChanged) {\n changed.push(entry);\n }\n }\n\n return changed;\n }\n\n // Get old file content - use oldPath for renames, otherwise current path\n const pathAtCommit = change.oldPath ?? change.path;\n const oldContent = await getFileAtCommit(pathAtCommit, markerCommit, this.cwd);\n\n // Build map of old entries\n const oldEntryMap = new Map<string, Entry>();\n if (oldContent) {\n try {\n // Detect file type from extension (markdown files contain embedded thalo blocks)\n const fileType = pathAtCommit.endsWith(\".md\") ? \"markdown\" : \"thalo\";\n const oldDoc = parseDocument(oldContent, { fileType });\n\n // Iterate all blocks (markdown files may have multiple thalo blocks)\n for (const block of oldDoc.blocks) {\n const oldAst = extractSourceFile(block.tree.rootNode);\n for (const entry of oldAst.entries) {\n const key = serializeIdentity(getEntryIdentity(entry));\n oldEntryMap.set(key, entry);\n }\n }\n } catch {\n // Parse error in old content - treat all current entries as changed\n }\n }\n\n // Find changed entries\n const changed: InstanceEntry[] = [];\n\n for (const entry of currentEntries) {\n // Check if entry matches any query\n let matchesQuery = false;\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n matchesQuery = true;\n break;\n }\n }\n\n if (!matchesQuery) {\n continue;\n }\n\n const key = serializeIdentity(getEntryIdentity(entry));\n const oldEntry = oldEntryMap.get(key);\n\n // Entry is changed if:\n // 1. It didn't exist at the marker commit (new entry)\n // 2. Its content differs from the marker commit (modified entry)\n if (!oldEntry || !entriesEqual(oldEntry, entry)) {\n changed.push(entry);\n }\n }\n\n return changed;\n }\n\n /**\n * Find a model in the workspace by relative path.\n * Normalizes path separators for cross-platform compatibility.\n *\n * Handles cases where:\n * - Git returns paths relative to repo root (e.g., \"project/entries.thalo\")\n * - Workspace has files relative to cwd (e.g., \"entries.thalo\")\n */\n private findModelByRelativePath(\n workspace: Workspace,\n relativePath: string,\n ): ReturnType<typeof workspace.getModel> {\n // Normalize to forward slashes for comparison (git always uses forward slashes)\n const normalizedRelative = relativePath.replace(/\\\\/g, \"/\");\n const relativeParts = normalizedRelative.split(\"/\");\n\n for (const model of workspace.allModels()) {\n const normalizedModel = model.file.replace(/\\\\/g, \"/\");\n\n // Exact match\n if (normalizedModel === normalizedRelative) {\n return model;\n }\n\n const modelParts = normalizedModel.split(\"/\");\n\n // Check if git path ends with model path (user running from subdirectory)\n // e.g., git returns \"project/entries.thalo\", model is \"entries.thalo\"\n if (relativeParts.length >= modelParts.length) {\n const relativeSuffix = relativeParts.slice(-modelParts.length).join(\"/\");\n if (relativeSuffix === normalizedModel) {\n return model;\n }\n }\n\n // Check if model path ends with git path (model has absolute/longer path)\n // e.g., model is \"/repo/project/entries.thalo\", git returns \"entries.thalo\"\n if (modelParts.length >= relativeParts.length) {\n const modelSuffix = modelParts.slice(-relativeParts.length).join(\"/\");\n if (modelSuffix === normalizedRelative) {\n return model;\n }\n }\n }\n return undefined;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"git-tracker.js","names":["changedEntries: InstanceEntry[]","results: InstanceEntry[]","changed: InstanceEntry[]","changed"],"sources":["../../../src/services/change-tracker/git-tracker.ts"],"sourcesContent":["import type { Entry, InstanceEntry } from \"../../ast/ast-types.js\";\nimport type { Query } from \"../query.js\";\nimport type { Workspace } from \"../../model/workspace.js\";\nimport {\n detectGitContext,\n getCurrentCommit,\n getFilesChangedSince,\n getFileAtCommit,\n commitExists,\n getUncommittedFiles,\n getBlameIgnoreRevs,\n getBlameCommitsForLineRange,\n isCommitAncestorOf,\n type FileChange,\n} from \"../../git/git.js\";\nimport { getEntryIdentity, serializeIdentity } from \"../../merge/entry-matcher.js\";\nimport { entriesEqual } from \"../../merge/entry-merger.js\";\nimport { entryMatchesQuery } from \"../query.js\";\nimport { parseDocument } from \"../../parser.node.js\";\nimport { extractSourceFile } from \"../../ast/extract.js\";\nimport type {\n ChangeTracker,\n ChangeMarker,\n ChangedEntriesResult,\n ChangeTrackerOptions,\n} from \"./change-tracker.js\";\nimport { UncommittedChangesError } from \"./change-tracker.js\";\n\n/**\n * Git-based change tracker.\n *\n * Uses git to determine which entries have changed since the last actualization.\n * This allows detection of in-place edits, not just new entries.\n *\n * Algorithm:\n * 1. Get files modified since the marker commit\n * 2. For each modified .thalo file:\n * - Get file content at the marker commit\n * - Parse both versions\n * - Compare entries by identity (linkId or timestamp)\n * - Mark entries as changed if they're new or content differs\n */\nexport class GitChangeTracker implements ChangeTracker {\n readonly type = \"git\" as const;\n private cwd: string;\n private force: boolean;\n private blameIgnoreRevs: string[] | null | undefined;\n\n constructor(options: ChangeTrackerOptions = {}) {\n this.cwd = options.cwd ?? process.cwd();\n this.force = options.force ?? false;\n }\n\n private async getIgnoreRevs(): Promise<string[] | null> {\n if (this.blameIgnoreRevs !== undefined) {\n return this.blameIgnoreRevs;\n }\n this.blameIgnoreRevs = await getBlameIgnoreRevs(this.cwd);\n return this.blameIgnoreRevs;\n }\n\n async getCurrentMarker(): Promise<ChangeMarker> {\n const commit = await getCurrentCommit(this.cwd);\n if (!commit) {\n throw new Error(\"Not in a git repository\");\n }\n return {\n type: \"git\",\n value: commit,\n };\n }\n\n async getChangedEntries(\n workspace: Workspace,\n queries: Query[],\n marker: ChangeMarker | null,\n ): Promise<ChangedEntriesResult> {\n // Validate git context\n const gitContext = await detectGitContext(this.cwd);\n if (!gitContext.isGitRepo) {\n throw new Error(\"Not in a git repository\");\n }\n\n const currentMarker = await this.getCurrentMarker();\n\n // Find files that contain matching entries (source files for the queries)\n const sourceFiles = this.getSourceFiles(workspace, queries);\n\n // Check for uncommitted changes in source files (unless force is set)\n if (!this.force && sourceFiles.length > 0) {\n const uncommitted = await getUncommittedFiles(this.cwd, sourceFiles);\n if (uncommitted.length > 0) {\n throw new UncommittedChangesError(uncommitted);\n }\n }\n\n // If no marker, return all matching entries (first run)\n if (!marker) {\n return {\n entries: this.getAllMatchingEntries(workspace, queries),\n currentMarker,\n };\n }\n\n // If marker is a timestamp, we can still handle it by returning all entries\n // (graceful fallback - user switched from timestamp to git tracking)\n if (marker.type === \"ts\") {\n return {\n entries: this.getAllMatchingEntries(workspace, queries),\n currentMarker,\n };\n }\n\n // Validate the commit exists\n const exists = await commitExists(marker.value, this.cwd);\n if (!exists) {\n // Commit doesn't exist (maybe rebased or squashed)\n // Return all matching entries as fallback\n return {\n entries: this.getAllMatchingEntries(workspace, queries),\n currentMarker,\n };\n }\n\n // Get files changed since the marker commit (with rename detection)\n const changedFiles = await getFilesChangedSince(marker.value, this.cwd);\n const thaloChanges = changedFiles.filter(\n (f) => f.path.endsWith(\".thalo\") || f.path.endsWith(\".md\"),\n );\n\n // If no thalo files changed, return empty\n if (thaloChanges.length === 0) {\n return {\n entries: [],\n currentMarker,\n };\n }\n\n // Find changed entries\n const changedEntries: InstanceEntry[] = [];\n const seenKeys = new Set<string>();\n\n for (const change of thaloChanges) {\n const entries = await this.getChangedEntriesInFile(workspace, change, marker.value, queries);\n\n for (const entry of entries) {\n // Include file path in dedup key to handle timestamp-based identities across files\n // (Link IDs are globally unique, but timestamps can collide across files)\n const key = `${change.path}:${serializeIdentity(getEntryIdentity(entry))}`;\n if (!seenKeys.has(key)) {\n seenKeys.add(key);\n changedEntries.push(entry);\n }\n }\n }\n\n return {\n entries: changedEntries,\n currentMarker,\n };\n }\n\n /**\n * Get all files that contain entries matching the queries.\n */\n private getSourceFiles(workspace: Workspace, queries: Query[]): string[] {\n const files = new Set<string>();\n\n for (const model of workspace.allModels()) {\n for (const entry of model.ast.entries) {\n if (entry.type !== \"instance_entry\") {\n continue;\n }\n\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n files.add(model.file);\n break;\n }\n }\n }\n }\n\n return Array.from(files);\n }\n\n /**\n * Get all entries matching the queries (for first run or fallback)\n */\n private getAllMatchingEntries(workspace: Workspace, queries: Query[]): InstanceEntry[] {\n const results: InstanceEntry[] = [];\n const seen = new Set<string>();\n\n for (const model of workspace.allModels()) {\n for (const entry of model.ast.entries) {\n if (entry.type !== \"instance_entry\") {\n continue;\n }\n\n const key = `${model.file}:${serializeIdentity(getEntryIdentity(entry))}`;\n\n if (seen.has(key)) {\n continue;\n }\n\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n results.push(entry);\n seen.add(key);\n break;\n }\n }\n }\n }\n\n return results;\n }\n\n /**\n * Get changed entries in a specific file.\n *\n * Note: This method only returns entries that exist in the current version of the file.\n * Deleted entries are intentionally not tracked because for the actualization use case,\n * we want entries to include in the synthesis prompt - deleted entries don't exist\n * and therefore don't need to be synthesized.\n */\n private async getChangedEntriesInFile(\n workspace: Workspace,\n change: FileChange,\n markerCommit: string,\n queries: Query[],\n ): Promise<InstanceEntry[]> {\n // Find the model for this file in the workspace (use current path)\n const model = this.findModelByRelativePath(workspace, change.path);\n if (!model) {\n return [];\n }\n\n // Get current entries\n const currentEntries = model.ast.entries.filter(\n (e): e is InstanceEntry => e.type === \"instance_entry\",\n );\n\n // If a blame ignore-revs file exists, use blame-based change detection.\n // This allows users to ignore formatting-only commits (e.g. via `.git-blame-ignore-revs`)\n // so they don't retrigger syntheses.\n const ignoreRevs = await this.getIgnoreRevs();\n if (ignoreRevs) {\n const changed: InstanceEntry[] = [];\n\n // Normalize end line: tree-sitter endPosition is exclusive.\n function locationToLineRange(entry: InstanceEntry): { startLine: number; endLine: number } {\n const startRow = entry.location.startPosition.row;\n const endRow = entry.location.endPosition.row;\n const endCol = entry.location.endPosition.column;\n const startLine = startRow + 1;\n const endLine = endCol === 0 ? endRow : endRow + 1;\n return { startLine, endLine: Math.max(startLine, endLine) };\n }\n\n for (const entry of currentEntries) {\n // Check if entry matches any query\n let matchesQuery = false;\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n matchesQuery = true;\n break;\n }\n }\n if (!matchesQuery) {\n continue;\n }\n\n const { startLine, endLine } = locationToLineRange(entry);\n const blamedCommits = await getBlameCommitsForLineRange(\n change.path,\n startLine,\n endLine,\n this.cwd,\n ignoreRevs,\n );\n\n let isChanged = false;\n for (const commit of blamedCommits) {\n // If the blamed commit is not an ancestor of the marker, then the entry\n // has changes \"after\" the marker (including merges), after applying ignore-revs.\n const isBeforeMarker = await isCommitAncestorOf(commit, markerCommit, this.cwd);\n if (!isBeforeMarker) {\n isChanged = true;\n break;\n }\n }\n\n if (isChanged) {\n changed.push(entry);\n }\n }\n\n return changed;\n }\n\n // Get old file content - use oldPath for renames, otherwise current path\n const pathAtCommit = change.oldPath ?? change.path;\n const oldContent = await getFileAtCommit(pathAtCommit, markerCommit, this.cwd);\n\n // Build map of old entries\n const oldEntryMap = new Map<string, Entry>();\n if (oldContent) {\n try {\n // Detect file type from extension (markdown files contain embedded thalo blocks)\n const fileType = pathAtCommit.endsWith(\".md\") ? \"markdown\" : \"thalo\";\n const oldDoc = parseDocument(oldContent, { fileType });\n\n // Iterate all blocks (markdown files may have multiple thalo blocks)\n for (const block of oldDoc.blocks) {\n const oldAst = extractSourceFile(block.tree.rootNode);\n for (const entry of oldAst.entries) {\n const key = serializeIdentity(getEntryIdentity(entry));\n oldEntryMap.set(key, entry);\n }\n }\n } catch {\n // Parse error in old content - treat all current entries as changed\n }\n }\n\n // Find changed entries\n const changed: InstanceEntry[] = [];\n\n for (const entry of currentEntries) {\n // Check if entry matches any query\n let matchesQuery = false;\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n matchesQuery = true;\n break;\n }\n }\n\n if (!matchesQuery) {\n continue;\n }\n\n const key = serializeIdentity(getEntryIdentity(entry));\n const oldEntry = oldEntryMap.get(key);\n\n // Entry is changed if:\n // 1. It didn't exist at the marker commit (new entry)\n // 2. Its content differs from the marker commit (modified entry)\n if (!oldEntry || !entriesEqual(oldEntry, entry)) {\n changed.push(entry);\n }\n }\n\n return changed;\n }\n\n /**\n * Find a model in the workspace by relative path.\n * Normalizes path separators for cross-platform compatibility.\n *\n * Handles cases where:\n * - Git returns paths relative to repo root (e.g., \"project/entries.thalo\")\n * - Workspace has files relative to cwd (e.g., \"entries.thalo\")\n */\n private findModelByRelativePath(\n workspace: Workspace,\n relativePath: string,\n ): ReturnType<typeof workspace.getModel> {\n // Normalize to forward slashes for comparison (git always uses forward slashes)\n const normalizedRelative = relativePath.replace(/\\\\/g, \"/\");\n const relativeParts = normalizedRelative.split(\"/\");\n\n for (const model of workspace.allModels()) {\n const normalizedModel = model.file.replace(/\\\\/g, \"/\");\n\n // Exact match\n if (normalizedModel === normalizedRelative) {\n return model;\n }\n\n const modelParts = normalizedModel.split(\"/\");\n\n // Check if git path ends with model path (user running from subdirectory)\n // e.g., git returns \"project/entries.thalo\", model is \"entries.thalo\"\n if (relativeParts.length >= modelParts.length) {\n const relativeSuffix = relativeParts.slice(-modelParts.length).join(\"/\");\n if (relativeSuffix === normalizedModel) {\n return model;\n }\n }\n\n // Check if model path ends with git path (model has absolute/longer path)\n // e.g., model is \"/repo/project/entries.thalo\", git returns \"entries.thalo\"\n if (modelParts.length >= relativeParts.length) {\n const modelSuffix = modelParts.slice(-relativeParts.length).join(\"/\");\n if (modelSuffix === normalizedRelative) {\n return model;\n }\n }\n }\n return undefined;\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rejot-dev/thalo",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -103,16 +103,12 @@
103
103
  "main": "./dist/mod.js",
104
104
  "types": "./dist/mod.d.ts",
105
105
  "dependencies": {
106
- "@rejot-dev/tree-sitter-thalo": "0.2.2"
106
+ "@rejot-dev/tree-sitter-thalo": "0.2.4"
107
107
  },
108
108
  "peerDependencies": {
109
- "tree-sitter": "^0.25.0",
110
109
  "web-tree-sitter": "^0.25.0"
111
110
  },
112
111
  "peerDependenciesMeta": {
113
- "tree-sitter": {
114
- "optional": true
115
- },
116
112
  "web-tree-sitter": {
117
113
  "optional": true
118
114
  }
package/dist/parser.js DELETED
@@ -1,27 +0,0 @@
1
- import { createParser } from "./parser.native.js";
2
-
3
- //#region src/parser.ts
4
- let parserInstance;
5
- /**
6
- * Get or create the singleton parser instance.
7
- *
8
- * @returns The singleton ThaloParser instance
9
- */
10
- function getParser() {
11
- if (!parserInstance) parserInstance = createParser();
12
- return parserInstance;
13
- }
14
- /**
15
- * Parse a document, automatically detecting if it's a .thalo file or markdown with embedded thalo blocks.
16
- *
17
- * @param source - The source code to parse
18
- * @param options - Parse options including fileType and filename
19
- * @returns A ParsedDocument containing one or more parsed blocks
20
- */
21
- const parseDocument = (source, options) => {
22
- return getParser().parseDocument(source, options);
23
- };
24
-
25
- //#endregion
26
- export { parseDocument };
27
- //# sourceMappingURL=parser.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"parser.js","names":["parserInstance: ThaloParser<Tree> | undefined","parseDocument: ThaloParser<Tree>[\"parseDocument\"]"],"sources":["../src/parser.ts"],"sourcesContent":["/**\n * Default parser entry point using native tree-sitter bindings.\n *\n * This module provides a singleton parser instance for convenience.\n * For more control, use `@rejot-dev/thalo/native` directly.\n */\n\nimport type { Tree } from \"tree-sitter\";\nimport { createParser, type ThaloParser } from \"./parser.native.js\";\n\n// Re-export types\nexport type {\n ParsedBlock,\n ParsedDocument,\n FileType,\n ParseOptions,\n ThaloParser,\n} from \"./parser.native.js\";\n\n// Lazy singleton parser instance\nlet parserInstance: ThaloParser<Tree> | undefined;\n\n/**\n * Get or create the singleton parser instance.\n *\n * @returns The singleton ThaloParser instance\n */\nfunction getParser(): ThaloParser<Tree> {\n if (!parserInstance) {\n parserInstance = createParser();\n }\n return parserInstance;\n}\n\n/**\n * Parse a thalo source string into a tree-sitter Tree.\n *\n * @param source - The thalo source code to parse\n * @returns The parsed tree-sitter Tree\n */\nexport const parseThalo = (source: string): Tree => {\n return getParser().parse(source);\n};\n\n/**\n * Parse a thalo source string with optional incremental parsing.\n *\n * When an oldTree is provided, tree-sitter can reuse unchanged parts of the\n * parse tree, making parsing much faster for small edits.\n *\n * Note: Before calling this with an oldTree, you must call oldTree.edit()\n * to inform tree-sitter about the changes made to the source.\n *\n * @param source - The thalo source code to parse\n * @param oldTree - Optional previous tree for incremental parsing\n * @returns The parsed tree-sitter Tree\n */\nexport const parseThaloIncremental = (source: string, oldTree?: Tree): Tree => {\n return getParser().parseIncremental(source, oldTree);\n};\n\n/**\n * Parse a document, automatically detecting if it's a .thalo file or markdown with embedded thalo blocks.\n *\n * @param source - The source code to parse\n * @param options - Parse options including fileType and filename\n * @returns A ParsedDocument containing one or more parsed blocks\n */\nexport const parseDocument: ThaloParser<Tree>[\"parseDocument\"] = (source, options) => {\n return getParser().parseDocument(source, options);\n};\n"],"mappings":";;;AAoBA,IAAIA;;;;;;AAOJ,SAAS,YAA+B;AACtC,KAAI,CAAC,eACH,kBAAiB,cAAc;AAEjC,QAAO;;;;;;;;;AAqCT,MAAaC,iBAAqD,QAAQ,YAAY;AACpF,QAAO,WAAW,CAAC,cAAc,QAAQ,QAAQ"}