@rejot-dev/thalo-lsp 0.0.0
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.
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/capabilities.d.ts +12 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +54 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/handlers/completions/completions.js +33 -0
- package/dist/handlers/completions/completions.js.map +1 -0
- package/dist/handlers/completions/context.js +228 -0
- package/dist/handlers/completions/context.js.map +1 -0
- package/dist/handlers/completions/providers/directive.js +50 -0
- package/dist/handlers/completions/providers/directive.js.map +1 -0
- package/dist/handlers/completions/providers/entity.js +52 -0
- package/dist/handlers/completions/providers/entity.js.map +1 -0
- package/dist/handlers/completions/providers/link.js +113 -0
- package/dist/handlers/completions/providers/link.js.map +1 -0
- package/dist/handlers/completions/providers/metadata-key.js +43 -0
- package/dist/handlers/completions/providers/metadata-key.js.map +1 -0
- package/dist/handlers/completions/providers/metadata-value.js +71 -0
- package/dist/handlers/completions/providers/metadata-value.js.map +1 -0
- package/dist/handlers/completions/providers/providers.js +31 -0
- package/dist/handlers/completions/providers/providers.js.map +1 -0
- package/dist/handlers/completions/providers/schema-block.js +46 -0
- package/dist/handlers/completions/providers/schema-block.js.map +1 -0
- package/dist/handlers/completions/providers/section.js +37 -0
- package/dist/handlers/completions/providers/section.js.map +1 -0
- package/dist/handlers/completions/providers/tag.js +55 -0
- package/dist/handlers/completions/providers/tag.js.map +1 -0
- package/dist/handlers/completions/providers/timestamp.js +32 -0
- package/dist/handlers/completions/providers/timestamp.js.map +1 -0
- package/dist/handlers/completions/providers/type-expr.js +56 -0
- package/dist/handlers/completions/providers/type-expr.js.map +1 -0
- package/dist/handlers/definition.js +166 -0
- package/dist/handlers/definition.js.map +1 -0
- package/dist/handlers/diagnostics.js +77 -0
- package/dist/handlers/diagnostics.js.map +1 -0
- package/dist/handlers/hover.js +73 -0
- package/dist/handlers/hover.js.map +1 -0
- package/dist/handlers/references.js +233 -0
- package/dist/handlers/references.js.map +1 -0
- package/dist/handlers/semantic-tokens.js +36 -0
- package/dist/handlers/semantic-tokens.js.map +1 -0
- package/dist/mod.d.ts +2 -0
- package/dist/mod.js +3 -0
- package/dist/server.bundled.js +1764 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +367 -0
- package/dist/server.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 - present ReJot Nederland B.V., and individual contributors.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Thalo Language Server
|
|
2
|
+
|
|
3
|
+
Language Server Protocol (LSP) implementation for **thalo** (Thought And Lore Language).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
| Feature | Status | Description |
|
|
8
|
+
| ------------------- | ------ | ------------------------------------------------ |
|
|
9
|
+
| Go to Definition | ✅ | Navigate to definitions (links, entities, etc.) |
|
|
10
|
+
| Find All References | ✅ | Find usages of links, tags, entities, and fields |
|
|
11
|
+
| Semantic Tokens | ✅ | Syntax highlighting via LSP |
|
|
12
|
+
| Diagnostics | ✅ | Validation errors and warnings |
|
|
13
|
+
| Hover | ✅ | Context-aware info for all syntax elements |
|
|
14
|
+
| Completions | ✅ | Schema-aware suggestions throughout entries |
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
The language server uses `@rejot-dev/thalo` for parsing and semantic analysis:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
┌─────────────────────────────────────────────────────────┐
|
|
22
|
+
│ IDE / Editor │
|
|
23
|
+
│ (VSCode, Neovim, etc.) │
|
|
24
|
+
└─────────────────────────┬───────────────────────────────┘
|
|
25
|
+
│ LSP Protocol
|
|
26
|
+
┌─────────────────────────▼───────────────────────────────┐
|
|
27
|
+
│ @rejot-dev/thalo-lsp │
|
|
28
|
+
│ ┌────────────────────────────────────────────────────┐ │
|
|
29
|
+
│ │ server.ts - LSP server lifecycle & routing │ │
|
|
30
|
+
│ └────────────────────────────────────────────────────┘ │
|
|
31
|
+
│ ┌────────────────────────────────────────────────────┐ │
|
|
32
|
+
│ │ handlers/ - Request handlers │ │
|
|
33
|
+
│ │ definition.ts - Go to definition │ │
|
|
34
|
+
│ │ references.ts - Find all references │ │
|
|
35
|
+
│ │ semantic-tokens.ts - Syntax highlighting │ │
|
|
36
|
+
│ │ diagnostics.ts - Validation errors │ │
|
|
37
|
+
│ │ hover.ts - Hover information │ │
|
|
38
|
+
│ │ completions/ - Autocomplete suggestions │ │
|
|
39
|
+
│ │ context.ts - Context detection │ │
|
|
40
|
+
│ │ providers/ - Modular completion providers │ │
|
|
41
|
+
│ └────────────────────────────────────────────────────┘ │
|
|
42
|
+
│ ┌────────────────────────────────────────────────────┐ │
|
|
43
|
+
│ │ capabilities.ts - LSP capability configuration │ │
|
|
44
|
+
│ └────────────────────────────────────────────────────┘ │
|
|
45
|
+
└─────────────────────────┬───────────────────────────────┘
|
|
46
|
+
│
|
|
47
|
+
┌─────────────────────────▼───────────────────────────────┐
|
|
48
|
+
│ @rejot-dev/thalo │
|
|
49
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌───────────────────┐ │
|
|
50
|
+
│ │ parser │ │ model │ │ services │ │
|
|
51
|
+
│ │ (parsing) │ │ (workspace) │ │ (definition, etc) │ │
|
|
52
|
+
│ └─────────────┘ └─────────────┘ └───────────────────┘ │
|
|
53
|
+
│ ┌─────────────┐ ┌──────────────────────────────────┐ │
|
|
54
|
+
│ │ checker │ │ semantic-tokens (highlighting) │ │
|
|
55
|
+
│ │ (diagnostics)│ └──────────────────────────────────┘ │
|
|
56
|
+
│ └─────────────┘ │
|
|
57
|
+
└─────────────────────────────────────────────────────────┘
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Build
|
|
64
|
+
pnpm build
|
|
65
|
+
|
|
66
|
+
# Type check
|
|
67
|
+
pnpm types:check
|
|
68
|
+
|
|
69
|
+
# Run tests
|
|
70
|
+
pnpm test
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
The server communicates over stdio:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Run the server (after building)
|
|
79
|
+
node dist/server.js --stdio
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Integration with Editors
|
|
83
|
+
|
|
84
|
+
The server can be integrated with any editor that supports LSP:
|
|
85
|
+
|
|
86
|
+
- **VSCode**: Use the `@rejot-dev/thalo-vscode` extension
|
|
87
|
+
- **Neovim**: Configure with `nvim-lspconfig`
|
|
88
|
+
- **Other editors**: Configure to run `thalo-lsp --stdio`
|
|
89
|
+
|
|
90
|
+
## Feature Details
|
|
91
|
+
|
|
92
|
+
### Go to Definition
|
|
93
|
+
|
|
94
|
+
Navigate to the definition of various syntax elements:
|
|
95
|
+
|
|
96
|
+
| Element | Navigates to |
|
|
97
|
+
| ----------------------------- | ----------------------------------- |
|
|
98
|
+
| `^link-id` | Entry where the link is defined |
|
|
99
|
+
| Entity name (`create lore`) | `define-entity` for that entity |
|
|
100
|
+
| Metadata key (`confidence:`) | Field definition in entity schema |
|
|
101
|
+
| Section header (`# Claim`) | Section definition in entity schema |
|
|
102
|
+
| `alter-entity` entity name | Original `define-entity` |
|
|
103
|
+
| `actualize-synthesis ^target` | `define-synthesis` target entry |
|
|
104
|
+
|
|
105
|
+
### Find All References
|
|
106
|
+
|
|
107
|
+
Find all usages of various syntax elements across the workspace:
|
|
108
|
+
|
|
109
|
+
| Element | Finds |
|
|
110
|
+
| ----------------------------- | ------------------------------------------- |
|
|
111
|
+
| `^link-id` | All references to that link |
|
|
112
|
+
| `#tag` | All entries with that tag |
|
|
113
|
+
| Entity name | All entries using that entity type |
|
|
114
|
+
| Metadata key | All entries using that field |
|
|
115
|
+
| Section header | All entries with that section |
|
|
116
|
+
| `define-entity` entity name | All usages + alter-entity entries |
|
|
117
|
+
| `define-synthesis` `^link-id` | All references + actualize-synthesis usages |
|
|
118
|
+
|
|
119
|
+
### Hover
|
|
120
|
+
|
|
121
|
+
Context-aware hover information for various syntax elements:
|
|
122
|
+
|
|
123
|
+
| Element | Information shown |
|
|
124
|
+
| --------------------- | --------------------------------------------------------------------- |
|
|
125
|
+
| `^link-id` | Entry title, metadata, tags, and file location |
|
|
126
|
+
| `#tag` | Usage count and list of entries with that tag |
|
|
127
|
+
| Directive | Documentation with syntax and examples |
|
|
128
|
+
| Entity name | Full schema: fields (with types), sections, and where defined |
|
|
129
|
+
| Metadata key | Field type, required/optional, default value, and description |
|
|
130
|
+
| Timestamp | Entry info and hint to add explicit link ID for referencing |
|
|
131
|
+
| Type expr | Documentation for `string`, `datetime`, `daterange`, `link`, `number` |
|
|
132
|
+
| Section header | Section description and required/optional status from schema |
|
|
133
|
+
| `define-synthesis` | Documentation for defining synthesis operations |
|
|
134
|
+
| `actualize-synthesis` | Documentation for triggering synthesis regeneration |
|
|
135
|
+
|
|
136
|
+
### Completions
|
|
137
|
+
|
|
138
|
+
Context-aware completions throughout the entry lifecycle:
|
|
139
|
+
|
|
140
|
+
| Context | Trigger | Suggests |
|
|
141
|
+
| --------------- | -------------------------- | ------------------------------------------------------------------------------ |
|
|
142
|
+
| Empty line | Start typing | Current timestamp (`2026-01-06T14:30`) |
|
|
143
|
+
| After timestamp | Space | All directives (`create`, `update`, `define-entity`, `define-synthesis`, etc.) |
|
|
144
|
+
| After directive | Space | Entity types (`lore`, `opinion`, `reference`, etc.) |
|
|
145
|
+
| After title | `^` or `#` | Link IDs or tags |
|
|
146
|
+
| Metadata key | Indented line | Field names from entity schema |
|
|
147
|
+
| Metadata value | After `key:` | Valid values (literals, `^self`, etc.) |
|
|
148
|
+
| Content section | `#` in content area | Section names from schema (`Claim`, `Reasoning`) |
|
|
149
|
+
| Schema block | `#` in schema entry | `# Metadata`, `# Sections`, etc. |
|
|
150
|
+
| Field type | After field name in schema | `string`, `datetime`, `link`, `daterange`, `number` |
|
|
151
|
+
| Link reference | `^` | All link IDs from workspace |
|
|
152
|
+
| Tag | `#` | Existing tags with usage counts |
|
|
153
|
+
|
|
154
|
+
**Schema-aware**: Metadata keys, values, and sections are pulled from entity schemas defined via
|
|
155
|
+
`define-entity`.
|
|
156
|
+
|
|
157
|
+
**Smart filtering**:
|
|
158
|
+
|
|
159
|
+
- Already-used metadata keys are excluded
|
|
160
|
+
- Required fields/sections sorted before optional
|
|
161
|
+
- Partial text filtering on all completions
|
|
162
|
+
|
|
163
|
+
### Diagnostics
|
|
164
|
+
|
|
165
|
+
Real-time validation errors using the `@rejot-dev/thalo` checker:
|
|
166
|
+
|
|
167
|
+
- Unresolved link references
|
|
168
|
+
- Unknown entity types
|
|
169
|
+
- Missing required fields/sections
|
|
170
|
+
- Invalid field types
|
|
171
|
+
- Duplicate link IDs
|
|
172
|
+
|
|
173
|
+
### Semantic Tokens
|
|
174
|
+
|
|
175
|
+
Provides rich syntax highlighting for:
|
|
176
|
+
|
|
177
|
+
- Timestamps
|
|
178
|
+
- Directives (`create`, `update`, `define-entity`, `alter-entity`, `define-synthesis`,
|
|
179
|
+
`actualize-synthesis`)
|
|
180
|
+
- Entity types
|
|
181
|
+
- Link references (with declaration modifier for definitions)
|
|
182
|
+
- Tags
|
|
183
|
+
- Metadata keys and values
|
|
184
|
+
- Section headers
|
|
185
|
+
|
|
186
|
+
## Limitations
|
|
187
|
+
|
|
188
|
+
### Not Yet Implemented
|
|
189
|
+
|
|
190
|
+
- **Rename Symbol**: No support for renaming link IDs, tags, or entities across files
|
|
191
|
+
- **Code Actions**: No quick fixes or refactoring actions
|
|
192
|
+
- **Workspace Symbols**: No symbol search across the workspace
|
|
193
|
+
- **Document Symbols**: No outline/breadcrumb support for entry structure
|
|
194
|
+
- **Folding Ranges**: No collapsible regions for entries or sections
|
|
195
|
+
- **Document Links**: No clickable links in content (only `^link-id` in metadata)
|
|
196
|
+
- **Signature Help**: No parameter hints (not applicable to thalo syntax)
|
|
197
|
+
- **Call Hierarchy**: No incoming/outgoing call navigation
|
|
198
|
+
- **Type Hierarchy**: No type relationship navigation
|
|
199
|
+
|
|
200
|
+
### Partial Support
|
|
201
|
+
|
|
202
|
+
- **Incremental Updates**: Full document re-parse on each edit (no incremental parsing)
|
|
203
|
+
- **Large Workspaces**: Performance may degrade with very large `.thalo` files (>10K lines)
|
|
204
|
+
- **Markdown Files**: Only `.thalo` code blocks within `.md` files are parsed; frontmatter is
|
|
205
|
+
ignored
|
|
206
|
+
- **Query Language**: The `sources:` field in `define-synthesis` entries is not semantically
|
|
207
|
+
validated
|
|
208
|
+
- **Synthesis Completions**: No specialized completions for synthesis query syntax (`where` clauses,
|
|
209
|
+
etc.)
|
|
210
|
+
|
|
211
|
+
### Known Issues
|
|
212
|
+
|
|
213
|
+
- Hover on timestamps shows entry info but timestamps are not valid link targets (use explicit
|
|
214
|
+
`^link-id`)
|
|
215
|
+
- Section header completions only appear when cursor is directly after `#` in content area
|
|
216
|
+
- Field type completions in schema entries don't suggest custom entity types for nested references
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SemanticTokensLegend } from "vscode-languageserver";
|
|
2
|
+
|
|
3
|
+
//#region src/capabilities.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Semantic tokens legend for LSP
|
|
7
|
+
* Maps the token types and modifiers from @rejot-dev/thalo
|
|
8
|
+
*/
|
|
9
|
+
declare const tokenLegend: SemanticTokensLegend;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { tokenLegend };
|
|
12
|
+
//# sourceMappingURL=capabilities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","names":[],"sources":["../src/capabilities.ts"],"sourcesContent":[],"mappings":";;;;;;;;cAwBa,aAAa"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { tokenModifiers, tokenTypes } from "@rejot-dev/thalo";
|
|
2
|
+
|
|
3
|
+
//#region src/capabilities.ts
|
|
4
|
+
/**
|
|
5
|
+
* File operation filters for thalo and markdown files
|
|
6
|
+
*/
|
|
7
|
+
const thaloFileFilters = [{
|
|
8
|
+
scheme: "file",
|
|
9
|
+
pattern: { glob: "**/*.thalo" }
|
|
10
|
+
}, {
|
|
11
|
+
scheme: "file",
|
|
12
|
+
pattern: { glob: "**/*.md" }
|
|
13
|
+
}];
|
|
14
|
+
/**
|
|
15
|
+
* Semantic tokens legend for LSP
|
|
16
|
+
* Maps the token types and modifiers from @rejot-dev/thalo
|
|
17
|
+
*/
|
|
18
|
+
const tokenLegend = {
|
|
19
|
+
tokenTypes: [...tokenTypes],
|
|
20
|
+
tokenModifiers: [...tokenModifiers]
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* LSP server capabilities for thalo
|
|
24
|
+
*
|
|
25
|
+
* Defines which LSP features are supported by this server.
|
|
26
|
+
*/
|
|
27
|
+
const serverCapabilities = {
|
|
28
|
+
textDocumentSync: {
|
|
29
|
+
openClose: true,
|
|
30
|
+
change: 2,
|
|
31
|
+
save: { includeText: true }
|
|
32
|
+
},
|
|
33
|
+
definitionProvider: true,
|
|
34
|
+
referencesProvider: true,
|
|
35
|
+
hoverProvider: true,
|
|
36
|
+
completionProvider: {
|
|
37
|
+
triggerCharacters: ["^", "#"],
|
|
38
|
+
resolveProvider: true
|
|
39
|
+
},
|
|
40
|
+
semanticTokensProvider: {
|
|
41
|
+
legend: tokenLegend,
|
|
42
|
+
full: true,
|
|
43
|
+
range: false
|
|
44
|
+
},
|
|
45
|
+
workspace: { fileOperations: {
|
|
46
|
+
didCreate: { filters: thaloFileFilters },
|
|
47
|
+
didDelete: { filters: thaloFileFilters },
|
|
48
|
+
didRename: { filters: thaloFileFilters }
|
|
49
|
+
} }
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
export { serverCapabilities, tokenLegend };
|
|
54
|
+
//# sourceMappingURL=capabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.js","names":["thaloFileFilters: FileOperationFilter[]","tokenLegend: SemanticTokensLegend","serverCapabilities: ServerCapabilities"],"sources":["../src/capabilities.ts"],"sourcesContent":["import type { ServerCapabilities, SemanticTokensLegend } from \"vscode-languageserver\";\nimport { tokenTypes, tokenModifiers } from \"@rejot-dev/thalo\";\n\n/**\n * File operation filter type for LSP file operations.\n * Describes which files the server is interested in for file operation notifications.\n */\ninterface FileOperationFilter {\n scheme?: string;\n pattern: { glob: string };\n}\n\n/**\n * File operation filters for thalo and markdown files\n */\nexport const thaloFileFilters: FileOperationFilter[] = [\n { scheme: \"file\", pattern: { glob: \"**/*.thalo\" } },\n { scheme: \"file\", pattern: { glob: \"**/*.md\" } },\n];\n\n/**\n * Semantic tokens legend for LSP\n * Maps the token types and modifiers from @rejot-dev/thalo\n */\nexport const tokenLegend: SemanticTokensLegend = {\n tokenTypes: [...tokenTypes],\n tokenModifiers: [...tokenModifiers],\n};\n\n/**\n * LSP server capabilities for thalo\n *\n * Defines which LSP features are supported by this server.\n */\nexport const serverCapabilities: ServerCapabilities = {\n // Document sync - full document sync for simplicity\n textDocumentSync: {\n openClose: true,\n change: 2, // Full sync\n save: {\n includeText: true,\n },\n },\n\n // Go to Definition - navigate to ^link-id definitions\n definitionProvider: true,\n\n // Find All References - find all usages of a ^link-id\n referencesProvider: true,\n\n // Hover - show link target details\n hoverProvider: true,\n\n // Completions - suggest ^link-ids and #tags\n completionProvider: {\n triggerCharacters: [\"^\", \"#\"],\n resolveProvider: true,\n },\n\n // Semantic tokens - syntax highlighting based on semantics\n semanticTokensProvider: {\n legend: tokenLegend,\n full: true,\n range: false,\n },\n\n // Workspace features - file operations for cross-file updates\n workspace: {\n fileOperations: {\n didCreate: { filters: thaloFileFilters },\n didDelete: { filters: thaloFileFilters },\n didRename: { filters: thaloFileFilters },\n },\n },\n};\n"],"mappings":";;;;;;AAeA,MAAaA,mBAA0C,CACrD;CAAE,QAAQ;CAAQ,SAAS,EAAE,MAAM,cAAc;CAAE,EACnD;CAAE,QAAQ;CAAQ,SAAS,EAAE,MAAM,WAAW;CAAE,CACjD;;;;;AAMD,MAAaC,cAAoC;CAC/C,YAAY,CAAC,GAAG,WAAW;CAC3B,gBAAgB,CAAC,GAAG,eAAe;CACpC;;;;;;AAOD,MAAaC,qBAAyC;CAEpD,kBAAkB;EAChB,WAAW;EACX,QAAQ;EACR,MAAM,EACJ,aAAa,MACd;EACF;CAGD,oBAAoB;CAGpB,oBAAoB;CAGpB,eAAe;CAGf,oBAAoB;EAClB,mBAAmB,CAAC,KAAK,IAAI;EAC7B,iBAAiB;EAClB;CAGD,wBAAwB;EACtB,QAAQ;EACR,MAAM;EACN,OAAO;EACR;CAGD,WAAW,EACT,gBAAgB;EACd,WAAW,EAAE,SAAS,kBAAkB;EACxC,WAAW,EAAE,SAAS,kBAAkB;EACxC,WAAW,EAAE,SAAS,kBAAkB;EACzC,EACF;CACF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { detectContext } from "./context.js";
|
|
2
|
+
import { allProviders } from "./providers/providers.js";
|
|
3
|
+
|
|
4
|
+
//#region src/handlers/completions/completions.ts
|
|
5
|
+
/**
|
|
6
|
+
* Handle textDocument/completion request.
|
|
7
|
+
*
|
|
8
|
+
* @param workspace - The thalo workspace for schema/link lookups
|
|
9
|
+
* @param document - The text document
|
|
10
|
+
* @param params - Completion parameters
|
|
11
|
+
* @returns Array of completion items
|
|
12
|
+
*/
|
|
13
|
+
function handleCompletion(workspace, document, params) {
|
|
14
|
+
const ctx = detectContext(document, params.position);
|
|
15
|
+
const items = [];
|
|
16
|
+
for (const provider of allProviders) if (provider.contextKinds.includes(ctx.kind)) items.push(...provider.getCompletions(ctx, workspace));
|
|
17
|
+
return items;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Handle completionItem/resolve request.
|
|
21
|
+
*
|
|
22
|
+
* Provides additional details for a completion item.
|
|
23
|
+
*
|
|
24
|
+
* @param item - The completion item to resolve
|
|
25
|
+
* @returns The resolved completion item
|
|
26
|
+
*/
|
|
27
|
+
function handleCompletionResolve(item) {
|
|
28
|
+
return item;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { handleCompletion, handleCompletionResolve };
|
|
33
|
+
//# sourceMappingURL=completions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"completions.js","names":["items: CompletionItem[]"],"sources":["../../../src/handlers/completions/completions.ts"],"sourcesContent":["/**\n * Completions handler for the thalo LSP.\n *\n * Provides intelligent completions for:\n * - Timestamps at line start\n * - Directives (create, update, define-entity, alter-entity)\n * - Entity types (built-in and schema-defined)\n * - Metadata keys and values (schema-aware)\n * - Links (^link-id) and tags (#tag)\n * - Content sections (schema-aware)\n * - Schema field types\n */\n\nimport type { CompletionItem, CompletionParams } from \"vscode-languageserver\";\nimport type { TextDocument } from \"vscode-languageserver-textdocument\";\nimport type { Workspace } from \"@rejot-dev/thalo\";\nimport { detectContext } from \"./context.js\";\nimport type { CompletionContext, CompletionContextKind } from \"./context.js\";\nimport { allProviders } from \"./providers/providers.js\";\n\n/**\n * A completion provider that handles one or more context kinds.\n */\nexport interface CompletionProvider {\n /** Human-readable name for debugging */\n readonly name: string;\n\n /** Which context kinds this provider handles */\n readonly contextKinds: readonly CompletionContextKind[];\n\n /**\n * Get completions for the given context.\n *\n * @param ctx - The completion context (cursor position, partial text, etc.)\n * @param workspace - The thalo workspace for schema/link lookups\n * @returns Array of completion items\n */\n getCompletions(ctx: CompletionContext, workspace: Workspace): CompletionItem[];\n}\n\n/**\n * Handle textDocument/completion request.\n *\n * @param workspace - The thalo workspace for schema/link lookups\n * @param document - The text document\n * @param params - Completion parameters\n * @returns Array of completion items\n */\nexport function handleCompletion(\n workspace: Workspace,\n document: TextDocument,\n params: CompletionParams,\n): CompletionItem[] {\n const ctx = detectContext(document, params.position);\n const items: CompletionItem[] = [];\n\n for (const provider of allProviders) {\n if (provider.contextKinds.includes(ctx.kind)) {\n items.push(...provider.getCompletions(ctx, workspace));\n }\n }\n\n return items;\n}\n\n/**\n * Handle completionItem/resolve request.\n *\n * Provides additional details for a completion item.\n *\n * @param item - The completion item to resolve\n * @returns The resolved completion item\n */\nexport function handleCompletionResolve(item: CompletionItem): CompletionItem {\n // For now, all details are included in the initial response\n return item;\n}\n\n// Re-export types and utilities for testing\nexport { detectContext, type CompletionContext, type CompletionContextKind } from \"./context.js\";\nexport { allProviders } from \"./providers/providers.js\";\n"],"mappings":";;;;;;;;;;;;AAgDA,SAAgB,iBACd,WACA,UACA,QACkB;CAClB,MAAM,MAAM,cAAc,UAAU,OAAO,SAAS;CACpD,MAAMA,QAA0B,EAAE;AAElC,MAAK,MAAM,YAAY,aACrB,KAAI,SAAS,aAAa,SAAS,IAAI,KAAK,CAC1C,OAAM,KAAK,GAAG,SAAS,eAAe,KAAK,UAAU,CAAC;AAI1D,QAAO;;;;;;;;;;AAWT,SAAgB,wBAAwB,MAAsC;AAE5E,QAAO"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { isSchemaDirective, isSynthesisDirective } from "@rejot-dev/thalo";
|
|
2
|
+
|
|
3
|
+
//#region src/handlers/completions/context.ts
|
|
4
|
+
/** Timestamp pattern: YYYY-MM-DDTHH:MMZ or YYYY-MM-DDTHH:MM±HH:MM */
|
|
5
|
+
const TIMESTAMP_PREFIX_PATTERN = /^[12]\d{3}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d(Z|[+-][0-2]\d:[0-5]\d)\s+/;
|
|
6
|
+
/**
|
|
7
|
+
* Get the text before the cursor on the current line.
|
|
8
|
+
*/
|
|
9
|
+
function getTextBeforeCursor(document, position) {
|
|
10
|
+
return document.getText({
|
|
11
|
+
start: {
|
|
12
|
+
line: position.line,
|
|
13
|
+
character: 0
|
|
14
|
+
},
|
|
15
|
+
end: position
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get the full text of a line.
|
|
20
|
+
*/
|
|
21
|
+
function getLineText(document, lineNumber) {
|
|
22
|
+
if (lineNumber >= document.lineCount) return "";
|
|
23
|
+
const start = {
|
|
24
|
+
line: lineNumber,
|
|
25
|
+
character: 0
|
|
26
|
+
};
|
|
27
|
+
const end = {
|
|
28
|
+
line: lineNumber + 1,
|
|
29
|
+
character: 0
|
|
30
|
+
};
|
|
31
|
+
return document.getText({
|
|
32
|
+
start,
|
|
33
|
+
end
|
|
34
|
+
}).replace(/\r?\n$/, "");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Find the entry header line by scanning backwards from the cursor.
|
|
38
|
+
* Returns the line number and text of the header, or undefined if not found.
|
|
39
|
+
*/
|
|
40
|
+
function findEntryHeader(document, fromLine) {
|
|
41
|
+
for (let i = fromLine; i >= 0; i--) {
|
|
42
|
+
const lineText = getLineText(document, i);
|
|
43
|
+
if (TIMESTAMP_PREFIX_PATTERN.test(lineText)) return {
|
|
44
|
+
line: i,
|
|
45
|
+
text: lineText
|
|
46
|
+
};
|
|
47
|
+
if (lineText.trim() && !lineText.startsWith(" ") && !lineText.startsWith(" ")) break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse entry info from a header line.
|
|
52
|
+
*/
|
|
53
|
+
function parseEntryInfo(headerText) {
|
|
54
|
+
const parts = headerText.split(/\s+/);
|
|
55
|
+
const info = {};
|
|
56
|
+
if (parts.length >= 2) {
|
|
57
|
+
const directive = parts[1];
|
|
58
|
+
info.directive = directive;
|
|
59
|
+
info.isSchemaEntry = isSchemaDirective(directive);
|
|
60
|
+
info.isSynthesisEntry = isSynthesisDirective(directive);
|
|
61
|
+
}
|
|
62
|
+
if (parts.length >= 3) if (info.isSynthesisEntry) info.entity = "synthesis";
|
|
63
|
+
else info.entity = parts[2];
|
|
64
|
+
return info;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extract existing metadata keys from lines between header and cursor.
|
|
68
|
+
*/
|
|
69
|
+
function extractExistingMetadataKeys(document, headerLine, currentLine) {
|
|
70
|
+
const keys = [];
|
|
71
|
+
for (let i = headerLine + 1; i < currentLine; i++) {
|
|
72
|
+
const match = getLineText(document, i).match(/^[\t ]+([a-z][a-zA-Z0-9\-_]*):/);
|
|
73
|
+
if (match) keys.push(match[1]);
|
|
74
|
+
}
|
|
75
|
+
return keys;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Check if a line is in the content area (after a blank line following metadata).
|
|
79
|
+
*/
|
|
80
|
+
function isInContentArea(document, headerLine, currentLine) {
|
|
81
|
+
let foundBlankAfterMetadata = false;
|
|
82
|
+
let foundMetadata = false;
|
|
83
|
+
for (let i = headerLine + 1; i < currentLine; i++) {
|
|
84
|
+
const line = getLineText(document, i);
|
|
85
|
+
if (!line.trim()) {
|
|
86
|
+
if (foundMetadata) foundBlankAfterMetadata = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (/^[\t ]+[a-z][a-zA-Z0-9\-_]*:/.test(line)) {
|
|
90
|
+
foundMetadata = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (foundBlankAfterMetadata && /^[\t ]+/.test(line)) return true;
|
|
94
|
+
}
|
|
95
|
+
return foundBlankAfterMetadata;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Check if we're inside a schema block definition (after # Metadata, # Sections, etc.)
|
|
99
|
+
*/
|
|
100
|
+
function isInSchemaBlock(document, headerLine, currentLine) {
|
|
101
|
+
for (let i = currentLine - 1; i > headerLine; i--) {
|
|
102
|
+
const trimmed = getLineText(document, i).trim();
|
|
103
|
+
if (trimmed === "# Metadata" || trimmed === "# Sections" || trimmed === "# Remove Metadata" || trimmed === "# Remove Sections") return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Detect the completion context at a given position.
|
|
109
|
+
*/
|
|
110
|
+
function detectContext(document, position) {
|
|
111
|
+
const textBefore = getTextBeforeCursor(document, position);
|
|
112
|
+
const ctx = {
|
|
113
|
+
kind: "unknown",
|
|
114
|
+
textBefore,
|
|
115
|
+
lineText: getLineText(document, position.line),
|
|
116
|
+
lineNumber: position.line,
|
|
117
|
+
entry: {},
|
|
118
|
+
partial: ""
|
|
119
|
+
};
|
|
120
|
+
if (textBefore.endsWith("^") || /\^[\w\-:]*$/.test(textBefore)) {
|
|
121
|
+
ctx.kind = "link";
|
|
122
|
+
const match = textBefore.match(/\^([\w\-:]*)$/);
|
|
123
|
+
ctx.partial = match ? match[1] : "";
|
|
124
|
+
const header = findEntryHeader(document, position.line);
|
|
125
|
+
if (header) ctx.entry = parseEntryInfo(header.text);
|
|
126
|
+
return ctx;
|
|
127
|
+
}
|
|
128
|
+
if (!textBefore.trimStart().startsWith("#") && (textBefore.endsWith("#") || /#[\w\-./]*$/.test(textBefore))) {
|
|
129
|
+
ctx.kind = "tag";
|
|
130
|
+
const match = textBefore.match(/#([\w\-./]*)$/);
|
|
131
|
+
ctx.partial = match ? match[1] : "";
|
|
132
|
+
const header = findEntryHeader(document, position.line);
|
|
133
|
+
if (header) ctx.entry = parseEntryInfo(header.text);
|
|
134
|
+
return ctx;
|
|
135
|
+
}
|
|
136
|
+
if (textBefore === "" && position.character === 0) {
|
|
137
|
+
ctx.kind = "line_start";
|
|
138
|
+
return ctx;
|
|
139
|
+
}
|
|
140
|
+
const timestampMatch = textBefore.match(/^([12]\d{3}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d(Z|[+-][0-2]\d:[0-5]\d))\s*/);
|
|
141
|
+
if (timestampMatch) {
|
|
142
|
+
const afterTimestamp = textBefore.slice(timestampMatch[0].length);
|
|
143
|
+
if (afterTimestamp === "" || afterTimestamp.match(/^\s*$/)) {
|
|
144
|
+
ctx.kind = "after_timestamp";
|
|
145
|
+
ctx.partial = afterTimestamp.trim();
|
|
146
|
+
return ctx;
|
|
147
|
+
}
|
|
148
|
+
const parts = afterTimestamp.trim().split(/\s+/);
|
|
149
|
+
const directive = parts[0];
|
|
150
|
+
ctx.entry.directive = directive;
|
|
151
|
+
ctx.entry.isSchemaEntry = isSchemaDirective(directive);
|
|
152
|
+
ctx.entry.isSynthesisEntry = isSynthesisDirective(directive);
|
|
153
|
+
if (parts.length === 1 && !afterTimestamp.endsWith(" ")) {
|
|
154
|
+
ctx.kind = "after_timestamp";
|
|
155
|
+
ctx.partial = directive;
|
|
156
|
+
return ctx;
|
|
157
|
+
}
|
|
158
|
+
if (parts.length === 1 && afterTimestamp.endsWith(" ")) {
|
|
159
|
+
ctx.kind = "after_directive";
|
|
160
|
+
ctx.partial = "";
|
|
161
|
+
return ctx;
|
|
162
|
+
}
|
|
163
|
+
if (parts.length >= 2) ctx.entry.entity = parts[1];
|
|
164
|
+
if (parts.length === 2 && !afterTimestamp.endsWith(" ") && !afterTimestamp.includes("\"")) {
|
|
165
|
+
ctx.kind = "after_directive";
|
|
166
|
+
ctx.partial = parts[1];
|
|
167
|
+
return ctx;
|
|
168
|
+
}
|
|
169
|
+
const titleMatch = afterTimestamp.match(/"[^"]*"/);
|
|
170
|
+
if (titleMatch) {
|
|
171
|
+
ctx.kind = "header_suffix";
|
|
172
|
+
ctx.partial = afterTimestamp.slice(afterTimestamp.indexOf(titleMatch[0]) + titleMatch[0].length).trim();
|
|
173
|
+
return ctx;
|
|
174
|
+
}
|
|
175
|
+
if (parts.length >= 2) {
|
|
176
|
+
ctx.kind = "after_entity";
|
|
177
|
+
return ctx;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const indentMatch = textBefore.match(/^([\t ]+)/);
|
|
181
|
+
if (indentMatch && indentMatch[1].length >= 2) {
|
|
182
|
+
const header = findEntryHeader(document, position.line);
|
|
183
|
+
if (header) {
|
|
184
|
+
ctx.entry = parseEntryInfo(header.text);
|
|
185
|
+
ctx.entry.existingMetadataKeys = extractExistingMetadataKeys(document, header.line, position.line);
|
|
186
|
+
}
|
|
187
|
+
const afterIndent = textBefore.slice(indentMatch[0].length);
|
|
188
|
+
if (ctx.entry.isSchemaEntry) {
|
|
189
|
+
if (afterIndent.startsWith("#") && !afterIndent.includes(":")) {
|
|
190
|
+
ctx.kind = "schema_block_header";
|
|
191
|
+
ctx.partial = afterIndent;
|
|
192
|
+
return ctx;
|
|
193
|
+
}
|
|
194
|
+
if (header && isInSchemaBlock(document, header.line, position.line)) {
|
|
195
|
+
const colonMatch = afterIndent.match(/^[a-z][a-zA-Z0-9\-_]*\??:\s*/);
|
|
196
|
+
if (colonMatch) {
|
|
197
|
+
ctx.kind = "field_type";
|
|
198
|
+
ctx.partial = afterIndent.slice(colonMatch[0].length);
|
|
199
|
+
return ctx;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (header && isInContentArea(document, header.line, position.line)) {
|
|
204
|
+
if (afterIndent.startsWith("#") && afterIndent.length <= 2) {
|
|
205
|
+
ctx.kind = "section_header";
|
|
206
|
+
ctx.partial = afterIndent.replace(/^#\s*/, "");
|
|
207
|
+
return ctx;
|
|
208
|
+
}
|
|
209
|
+
ctx.kind = "unknown";
|
|
210
|
+
return ctx;
|
|
211
|
+
}
|
|
212
|
+
const colonIndex = afterIndent.indexOf(":");
|
|
213
|
+
if (colonIndex !== -1) {
|
|
214
|
+
ctx.kind = "metadata_value";
|
|
215
|
+
ctx.metadataKey = afterIndent.slice(0, colonIndex).trim();
|
|
216
|
+
ctx.partial = afterIndent.slice(colonIndex + 1).trim();
|
|
217
|
+
return ctx;
|
|
218
|
+
}
|
|
219
|
+
ctx.kind = "metadata_key";
|
|
220
|
+
ctx.partial = afterIndent.trim();
|
|
221
|
+
return ctx;
|
|
222
|
+
}
|
|
223
|
+
return ctx;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
//#endregion
|
|
227
|
+
export { detectContext };
|
|
228
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","names":["info: EntryInfo","keys: string[]","ctx: CompletionContext"],"sources":["../../../src/handlers/completions/context.ts"],"sourcesContent":["import type { Position } from \"vscode-languageserver\";\nimport type { TextDocument } from \"vscode-languageserver-textdocument\";\nimport { isSchemaDirective, isSynthesisDirective } from \"@rejot-dev/thalo\";\n\n// ===================\n// Context Kinds\n// ===================\n\n/**\n * The kind of completion context detected at the cursor position.\n */\nexport type CompletionContextKind =\n | \"line_start\" // Empty line at column 0, suggest timestamp\n | \"after_timestamp\" // After timestamp, suggest directive\n | \"after_directive\" // After directive, suggest entity/identifier\n | \"after_entity\" // After entity, suggest title (no completion typically)\n | \"header_suffix\" // After title, suggest ^link or #tag\n | \"metadata_key\" // Indented line without colon, suggest metadata keys\n | \"metadata_value\" // After key:, suggest values\n | \"schema_block_header\" // In schema entry, suggest # Metadata etc.\n | \"field_type\" // In schema field definition after :, suggest types\n | \"section_header\" // Content area # line, suggest section names\n | \"link\" // After ^, suggest link IDs\n | \"tag\" // After #, suggest tags\n | \"unknown\";\n\n// ===================\n// Context Interface\n// ===================\n\n/**\n * Information about the entry being edited.\n */\nexport interface EntryInfo {\n /** The directive (create, update, define-entity, alter-entity, define-synthesis, actualize-synthesis) */\n directive?: string;\n /** The entity type (lore, opinion, etc.) or entity name for schema entries */\n entity?: string;\n /** Whether this is a schema entry (define-entity/alter-entity) */\n isSchemaEntry?: boolean;\n /** Whether this is a synthesis entry (define-synthesis/actualize-synthesis) */\n isSynthesisEntry?: boolean;\n /** Metadata keys already present in the entry */\n existingMetadataKeys?: string[];\n}\n\n/**\n * Completion context with all information needed by providers.\n */\nexport interface CompletionContext {\n /** The detected context kind */\n kind: CompletionContextKind;\n /** Text before the cursor on the current line */\n textBefore: string;\n /** Full text of the current line */\n lineText: string;\n /** The current line number (0-based) */\n lineNumber: number;\n /** Information about the entry being edited */\n entry: EntryInfo;\n /** Partial text being typed (for filtering) */\n partial: string;\n /** The metadata key if in metadata_value context */\n metadataKey?: string;\n}\n\n// ===================\n// Context Detection\n// ===================\n\n/** Timestamp pattern: YYYY-MM-DDTHH:MMZ or YYYY-MM-DDTHH:MM±HH:MM */\nconst TIMESTAMP_PREFIX_PATTERN =\n /^[12]\\d{3}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d(Z|[+-][0-2]\\d:[0-5]\\d)\\s+/;\n\n/**\n * Get the text before the cursor on the current line.\n */\nexport function getTextBeforeCursor(document: TextDocument, position: Position): string {\n return document.getText({\n start: { line: position.line, character: 0 },\n end: position,\n });\n}\n\n/**\n * Get the full text of a line.\n */\nexport function getLineText(document: TextDocument, lineNumber: number): string {\n const lineCount = document.lineCount;\n if (lineNumber >= lineCount) {\n return \"\";\n }\n const start = { line: lineNumber, character: 0 };\n const end = { line: lineNumber + 1, character: 0 };\n const text = document.getText({ start, end });\n // Remove trailing newline\n return text.replace(/\\r?\\n$/, \"\");\n}\n\n/**\n * Find the entry header line by scanning backwards from the cursor.\n * Returns the line number and text of the header, or undefined if not found.\n */\nfunction findEntryHeader(\n document: TextDocument,\n fromLine: number,\n): { line: number; text: string } | undefined {\n for (let i = fromLine; i >= 0; i--) {\n const lineText = getLineText(document, i);\n // Header lines start with a timestamp (not indented)\n if (TIMESTAMP_PREFIX_PATTERN.test(lineText)) {\n return { line: i, text: lineText };\n }\n // If we hit a non-indented, non-empty line that's not a timestamp, stop\n if (lineText.trim() && !lineText.startsWith(\" \") && !lineText.startsWith(\"\\t\")) {\n break;\n }\n }\n return undefined;\n}\n\n/**\n * Parse entry info from a header line.\n */\nfunction parseEntryInfo(headerText: string): EntryInfo {\n const parts = headerText.split(/\\s+/);\n const info: EntryInfo = {};\n\n if (parts.length >= 2) {\n const directive = parts[1];\n info.directive = directive;\n info.isSchemaEntry = isSchemaDirective(directive);\n info.isSynthesisEntry = isSynthesisDirective(directive);\n }\n\n if (parts.length >= 3) {\n // For synthesis entries, part[2] is either a title or ^link\n if (info.isSynthesisEntry) {\n info.entity = \"synthesis\";\n } else {\n info.entity = parts[2];\n }\n }\n\n return info;\n}\n\n/**\n * Extract existing metadata keys from lines between header and cursor.\n */\nfunction extractExistingMetadataKeys(\n document: TextDocument,\n headerLine: number,\n currentLine: number,\n): string[] {\n const keys: string[] = [];\n for (let i = headerLine + 1; i < currentLine; i++) {\n const line = getLineText(document, i);\n // Match indented lines with key: pattern\n const match = line.match(/^[\\t ]+([a-z][a-zA-Z0-9\\-_]*):/);\n if (match) {\n keys.push(match[1]);\n }\n }\n return keys;\n}\n\n/**\n * Check if a line is in the content area (after a blank line following metadata).\n */\nfunction isInContentArea(document: TextDocument, headerLine: number, currentLine: number): boolean {\n let foundBlankAfterMetadata = false;\n let foundMetadata = false;\n\n for (let i = headerLine + 1; i < currentLine; i++) {\n const line = getLineText(document, i);\n const trimmed = line.trim();\n\n if (!trimmed) {\n if (foundMetadata) {\n foundBlankAfterMetadata = true;\n }\n continue;\n }\n\n // Check if it's a metadata line (indented with key:)\n if (/^[\\t ]+[a-z][a-zA-Z0-9\\-_]*:/.test(line)) {\n foundMetadata = true;\n continue;\n }\n\n // Check if it's content (indented text after blank line)\n if (foundBlankAfterMetadata && /^[\\t ]+/.test(line)) {\n return true;\n }\n }\n\n return foundBlankAfterMetadata;\n}\n\n/**\n * Check if we're inside a schema block definition (after # Metadata, # Sections, etc.)\n */\nfunction isInSchemaBlock(document: TextDocument, headerLine: number, currentLine: number): boolean {\n for (let i = currentLine - 1; i > headerLine; i--) {\n const line = getLineText(document, i);\n const trimmed = line.trim();\n\n // Found a schema block header\n if (\n trimmed === \"# Metadata\" ||\n trimmed === \"# Sections\" ||\n trimmed === \"# Remove Metadata\" ||\n trimmed === \"# Remove Sections\"\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Detect the completion context at a given position.\n */\nexport function detectContext(document: TextDocument, position: Position): CompletionContext {\n const textBefore = getTextBeforeCursor(document, position);\n const lineText = getLineText(document, position.line);\n\n // Default context\n const ctx: CompletionContext = {\n kind: \"unknown\",\n textBefore,\n lineText,\n lineNumber: position.line,\n entry: {},\n partial: \"\",\n };\n\n // Check for link context (^) - highest priority\n if (textBefore.endsWith(\"^\") || /\\^[\\w\\-:]*$/.test(textBefore)) {\n ctx.kind = \"link\";\n const match = textBefore.match(/\\^([\\w\\-:]*)$/);\n ctx.partial = match ? match[1] : \"\";\n // Still try to get entry info\n const header = findEntryHeader(document, position.line);\n if (header) {\n ctx.entry = parseEntryInfo(header.text);\n }\n return ctx;\n }\n\n // Check for tag context (#) - but not markdown headers in content\n const trimmedBefore = textBefore.trimStart();\n if (\n !trimmedBefore.startsWith(\"#\") &&\n (textBefore.endsWith(\"#\") || /#[\\w\\-./]*$/.test(textBefore))\n ) {\n ctx.kind = \"tag\";\n const match = textBefore.match(/#([\\w\\-./]*)$/);\n ctx.partial = match ? match[1] : \"\";\n // Still try to get entry info\n const header = findEntryHeader(document, position.line);\n if (header) {\n ctx.entry = parseEntryInfo(header.text);\n }\n return ctx;\n }\n\n // Empty line at column 0 -> suggest timestamp\n if (textBefore === \"\" && position.character === 0) {\n ctx.kind = \"line_start\";\n return ctx;\n }\n\n // Check if line starts with timestamp\n const timestampMatch = textBefore.match(\n /^([12]\\d{3}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d(Z|[+-][0-2]\\d:[0-5]\\d))\\s*/,\n );\n if (timestampMatch) {\n const afterTimestamp = textBefore.slice(timestampMatch[0].length);\n\n // Just timestamp + space -> suggest directive\n if (afterTimestamp === \"\" || afterTimestamp.match(/^\\s*$/)) {\n ctx.kind = \"after_timestamp\";\n ctx.partial = afterTimestamp.trim();\n return ctx;\n }\n\n // Parse what comes after timestamp\n const parts = afterTimestamp.trim().split(/\\s+/);\n const directive = parts[0];\n\n ctx.entry.directive = directive;\n ctx.entry.isSchemaEntry = isSchemaDirective(directive);\n ctx.entry.isSynthesisEntry = isSynthesisDirective(directive);\n\n // After directive -> suggest entity\n if (parts.length === 1 && !afterTimestamp.endsWith(\" \")) {\n ctx.kind = \"after_timestamp\";\n ctx.partial = directive;\n return ctx;\n }\n\n if (parts.length === 1 && afterTimestamp.endsWith(\" \")) {\n ctx.kind = \"after_directive\";\n ctx.partial = \"\";\n return ctx;\n }\n\n // Entity is being typed\n if (parts.length >= 2) {\n ctx.entry.entity = parts[1];\n }\n\n if (parts.length === 2 && !afterTimestamp.endsWith(\" \") && !afterTimestamp.includes('\"')) {\n ctx.kind = \"after_directive\";\n ctx.partial = parts[1];\n return ctx;\n }\n\n // Check if we're after the title (look for closing quote)\n const titleMatch = afterTimestamp.match(/\"[^\"]*\"/);\n if (titleMatch) {\n // After title -> header suffix (for tags/links)\n ctx.kind = \"header_suffix\";\n const afterTitle = afterTimestamp.slice(\n afterTimestamp.indexOf(titleMatch[0]) + titleMatch[0].length,\n );\n ctx.partial = afterTitle.trim();\n return ctx;\n }\n\n // After entity but before/during title\n if (parts.length >= 2) {\n ctx.kind = \"after_entity\";\n return ctx;\n }\n }\n\n // Indented line -> metadata or content\n const indentMatch = textBefore.match(/^([\\t ]+)/);\n if (indentMatch && indentMatch[1].length >= 2) {\n const header = findEntryHeader(document, position.line);\n if (header) {\n ctx.entry = parseEntryInfo(header.text);\n ctx.entry.existingMetadataKeys = extractExistingMetadataKeys(\n document,\n header.line,\n position.line,\n );\n }\n\n const afterIndent = textBefore.slice(indentMatch[0].length);\n\n // Schema entry handling\n if (ctx.entry.isSchemaEntry) {\n // Check for # at start of indented content -> schema block header\n if (afterIndent.startsWith(\"#\") && !afterIndent.includes(\":\")) {\n ctx.kind = \"schema_block_header\";\n ctx.partial = afterIndent;\n return ctx;\n }\n\n // Inside a schema block (after # Metadata, # Sections, etc.)\n if (header && isInSchemaBlock(document, header.line, position.line)) {\n // Check if we're after a colon (field type position)\n const colonMatch = afterIndent.match(/^[a-z][a-zA-Z0-9\\-_]*\\??:\\s*/);\n if (colonMatch) {\n ctx.kind = \"field_type\";\n ctx.partial = afterIndent.slice(colonMatch[0].length);\n return ctx;\n }\n }\n }\n\n // Check if we're in content area\n if (header && isInContentArea(document, header.line, position.line)) {\n // Check for section header (# in content)\n if (afterIndent.startsWith(\"#\") && afterIndent.length <= 2) {\n ctx.kind = \"section_header\";\n ctx.partial = afterIndent.replace(/^#\\s*/, \"\");\n return ctx;\n }\n // Regular content - no completion\n ctx.kind = \"unknown\";\n return ctx;\n }\n\n // Check if there's a colon -> metadata value\n const colonIndex = afterIndent.indexOf(\":\");\n if (colonIndex !== -1) {\n ctx.kind = \"metadata_value\";\n ctx.metadataKey = afterIndent.slice(0, colonIndex).trim();\n ctx.partial = afterIndent.slice(colonIndex + 1).trim();\n return ctx;\n }\n\n // No colon yet -> metadata key\n ctx.kind = \"metadata_key\";\n ctx.partial = afterIndent.trim();\n return ctx;\n }\n\n return ctx;\n}\n\n/**\n * Get partial text for filtering (used by some providers).\n */\nexport function getPartialText(textBefore: string, prefix: string): string {\n const match = textBefore.match(new RegExp(`\\\\${prefix}([\\\\w\\\\-:/.]*)$`));\n return match ? match[1] : \"\";\n}\n"],"mappings":"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"}
|