@xano/xanoscript-language-server 11.9.1 → 11.10.1

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.
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: no-git-interactions
3
+ description: User does not want Claude to interact with git (no commits, no staging, no git commands)
4
+ type: feedback
5
+ ---
6
+
7
+ Do not interact with git — no commits, no staging, no git commands. Focus purely on implementation and testing.
8
+
9
+ **Why:** User manages their own git workflow.
10
+
11
+ **How to apply:** Skip all git-related steps in plans. When dispatching subagents, explicitly tell them not to run any git commands. Remove commit steps from task instructions.
@@ -2,18 +2,13 @@
2
2
  "permissions": {
3
3
  "allow": [
4
4
  "Bash(npm test:*)",
5
- "Bash(node -e:*)",
6
- "Bash(node --input-type=module -e:*)",
7
5
  "Bash(git checkout:*)",
8
6
  "Bash(ls:*)",
9
7
  "Bash(npm run lint:*)",
10
8
  "Bash(grep:*)",
11
- "Bash(node --input-type=module:*)",
12
9
  "Bash(find:*)",
13
10
  "Read(//tmp/**)",
14
11
  "Bash(git stash:*)",
15
- "Bash(node:*)",
16
- "Bash(for:*)",
17
12
  "Bash(do)",
18
13
  "Bash(if ! grep -q \"guidFieldAttribute\" \"$f\")",
19
14
  "Bash(then)",
@@ -1,4 +1,5 @@
1
1
  import { lexDocument } from "../lexer/lexer.js";
2
+ import { multidocParser } from "../parser/multidoc.js";
2
3
  import { xanoscriptParser } from "../parser/parser.js";
3
4
  import { getSchemeFromContent } from "../utils.js";
4
5
 
@@ -34,14 +35,30 @@ class DocumentCache {
34
35
  cached.version === version &&
35
36
  cached.textLength === textLength
36
37
  ) {
37
- // Re-lex (cheap) to provide tokens; return cached parser state
38
38
  return {
39
- lexResult: lexDocument(text),
39
+ lexResult: cached.scheme === "multidoc" ? { tokens: [] } : lexDocument(text),
40
40
  parser: cached.parserState,
41
41
  scheme: cached.scheme,
42
42
  };
43
43
  }
44
44
 
45
+ // Multidoc detection — split on \n---\n separator
46
+ if (text.includes("\n---\n")) {
47
+ const parserState = multidocParser(text);
48
+ const cacheEntry = {
49
+ version,
50
+ textLength,
51
+ parserState,
52
+ scheme: "multidoc",
53
+ };
54
+ this.cache.set(uri, cacheEntry);
55
+ return {
56
+ lexResult: { tokens: [] },
57
+ parser: parserState,
58
+ scheme: "multidoc",
59
+ };
60
+ }
61
+
45
62
  // Parse and cache a snapshot of the parser state
46
63
  const scheme = getSchemeFromContent(text);
47
64
  const lexResult = lexDocument(text);
@@ -11,6 +11,7 @@ import { onDidChangeContent } from "./onDidChangeContent/onDidChangeContent.js";
11
11
  import { onHover } from "./onHover/onHover.js";
12
12
  import { onSemanticCheck } from "./onSemanticCheck/onSemanticCheck.js";
13
13
  import { TOKEN_TYPES } from "./onSemanticCheck/tokens.js";
14
+ import pkg from "./package.json" with { type: "json" };
14
15
 
15
16
  const messageReader = new BrowserMessageReader(self);
16
17
  const messageWriter = new BrowserMessageWriter(self);
@@ -47,6 +48,6 @@ connection.onRequest("textDocument/semanticTokens/full", (params) =>
47
48
  documents.onDidChangeContent((params) =>
48
49
  onDidChangeContent(params, connection)
49
50
  );
50
- connection.onInitialized(() => console.log("lang server init"));
51
+ connection.onInitialized(() => console.log(`XanoScript Language Server v${pkg.version}`));
51
52
  documents.listen(connection);
52
53
  connection.listen();
@@ -1,4 +1,6 @@
1
+ import { documentCache } from "../cache/documentCache.js";
1
2
  import { mapToVirtualJS } from "../embedded/embeddedContent.js";
3
+ import { findSegmentAtOffset } from "../parser/multidoc.js";
2
4
  import { getSchemeFromContent } from "../utils.js";
3
5
  import { workspaceIndex } from "../workspace/workspaceIndex.js";
4
6
  import { getContentAssistSuggestions } from "./contentAssist.js";
@@ -32,9 +34,28 @@ export function onCompletion(params, documents) {
32
34
  return null;
33
35
  }
34
36
 
35
- // Otherwise, handle as regular XanoScript
36
- const scheme = getSchemeFromContent(text);
37
- const prefix = text.slice(0, offset);
37
+ // For multidoc, resolve to the correct segment
38
+ const { parser } = documentCache.getOrParse(
39
+ params.textDocument.uri,
40
+ document.version,
41
+ text,
42
+ );
43
+
44
+ let scheme;
45
+ let prefix;
46
+
47
+ if (parser.isMultidoc) {
48
+ const match = findSegmentAtOffset(parser, offset);
49
+ if (!match) return null;
50
+ scheme = getSchemeFromContent(match.segment.text);
51
+ // Prefix is everything in the full text up to the cursor — contentAssist
52
+ // parses backwards from the cursor to find context, so it needs the segment
53
+ // text up to the local offset
54
+ prefix = match.segment.text.slice(0, match.localOffset);
55
+ } else {
56
+ scheme = getSchemeFromContent(text);
57
+ prefix = text.slice(0, offset);
58
+ }
38
59
 
39
60
  const suggestions = getContentAssistSuggestions(prefix, scheme);
40
61
 
@@ -73,6 +73,10 @@ export function onDidChangeContent(params, connection, features = {}) {
73
73
  const text = document.getText();
74
74
 
75
75
  try {
76
+ // Capture previous segment count before getOrParse overwrites the cache
77
+ const prevCached = documentCache.cache.get(document.uri);
78
+ const prevSegmentCount = prevCached?.parserState?.segmentCount ?? 0;
79
+
76
80
  // Parse the XanoScript file using cache
77
81
  const { lexResult, parser, scheme } = documentCache.getOrParse(
78
82
  document.uri,
@@ -80,8 +84,28 @@ export function onDidChangeContent(params, connection, features = {}) {
80
84
  text,
81
85
  );
82
86
 
83
- // Update workspace index with already-parsed data (no re-parse needed)
84
- workspaceIndex.addParsed(document.uri, text, parser.__symbolTable);
87
+ // Update workspace index
88
+ // Clean up old fragment URIs from previous parse
89
+ for (let i = 0; i < prevSegmentCount; i++) {
90
+ workspaceIndex.removeFile(`${document.uri}#${i}`);
91
+ }
92
+ if (parser.isMultidoc) {
93
+ // Remove the base URI in case document switched from single to multidoc
94
+ workspaceIndex.removeFile(document.uri);
95
+ // Index each segment with fragment URIs
96
+ if (parser.segments) {
97
+ for (let i = 0; i < parser.segments.length; i++) {
98
+ const seg = parser.segments[i];
99
+ workspaceIndex.addParsed(
100
+ `${document.uri}#${i}`,
101
+ seg.text,
102
+ seg.symbolTable,
103
+ );
104
+ }
105
+ }
106
+ } else {
107
+ workspaceIndex.addParsed(document.uri, text, parser.__symbolTable);
108
+ }
85
109
 
86
110
  for (const error of parser.errors) {
87
111
  console.error(
@@ -92,8 +116,8 @@ export function onDidChangeContent(params, connection, features = {}) {
92
116
  // Create diagnostics in a single pass
93
117
  const diagnostics = createDiagnostics(parser, document);
94
118
 
95
- // Run cross-file validation and append as warnings
96
- if (features.crossFileValidation !== false && parser.__symbolTable?.references) {
119
+ // Run cross-file validation (skip for multidoc — already done internally)
120
+ if (!parser.isMultidoc && features.crossFileValidation !== false && parser.__symbolTable?.references) {
97
121
  const crossFileWarnings = crossFileValidate(
98
122
  parser.__symbolTable.references,
99
123
  workspaceIndex,
@@ -110,8 +134,8 @@ export function onDidChangeContent(params, connection, features = {}) {
110
134
  }
111
135
  }
112
136
 
113
- // Run variable validation and append warnings/hints
114
- if (features.variableValidation !== false && parser.__symbolTable?.varDeclarations) {
137
+ // Run variable validation (skip for multidoc — already done per-segment internally)
138
+ if (!parser.isMultidoc && features.variableValidation !== false && parser.__symbolTable?.varDeclarations) {
115
139
  const varResult = validateVariables(
116
140
  parser.__symbolTable,
117
141
  lexResult.tokens,
@@ -1,4 +1,6 @@
1
1
  import { documentCache } from "../cache/documentCache.js";
2
+ import { lexDocument } from "../lexer/lexer.js";
3
+ import { findSegmentAtOffset } from "../parser/multidoc.js";
2
4
 
3
5
  /**
4
6
  * Binary search to find token index at the given offset.
@@ -54,9 +56,50 @@ export function onHoverDocument(params, documents, hoverProviders = []) {
54
56
  text,
55
57
  );
56
58
 
59
+ let tokens;
60
+ let hoverParser;
61
+ let tokenOffsetAdjustment = 0;
62
+
63
+ if (parser.isMultidoc) {
64
+ // For multidoc, find the segment at the cursor position and lex it
65
+ const match = findSegmentAtOffset(parser, offset);
66
+ if (!match) return null;
67
+
68
+ const segLexResult = lexDocument(match.segment.text);
69
+ if (segLexResult.errors.length > 0) return null;
70
+
71
+ tokens = segLexResult.tokens;
72
+ hoverParser = match.segment.parser;
73
+ tokenOffsetAdjustment = match.segment.globalOffset;
74
+
75
+ // Find token using segment-local offset
76
+ const tokenIdx = findTokenAtOffset(tokens, match.localOffset);
77
+ if (tokenIdx === -1) return null;
78
+
79
+ const messageProvider = hoverProviders.find((provider) =>
80
+ provider.isMatch(tokenIdx, tokens, hoverParser),
81
+ );
82
+
83
+ if (messageProvider) {
84
+ return {
85
+ contents: {
86
+ kind: "markdown",
87
+ value: messageProvider.render(tokenIdx, tokens, hoverParser),
88
+ },
89
+ range: {
90
+ // Convert segment-local token offsets to global document positions
91
+ start: document.positionAt(tokens[tokenIdx].startOffset + tokenOffsetAdjustment),
92
+ end: document.positionAt(tokens[tokenIdx].endOffset + 1 + tokenOffsetAdjustment),
93
+ },
94
+ };
95
+ }
96
+
97
+ return null;
98
+ }
99
+
57
100
  if (lexResult.errors.length > 0) return null;
58
101
 
59
- const tokens = lexResult.tokens;
102
+ tokens = lexResult.tokens;
60
103
 
61
104
  // Find the token under the cursor using binary search (O(log n))
62
105
  const tokenIdx = findTokenAtOffset(tokens, offset);
@@ -452,6 +452,180 @@ describe("onHoverDocument", () => {
452
452
  expect(result).to.have.property("range");
453
453
  });
454
454
 
455
+ it("should return hover info for var in second segment of multidoc", () => {
456
+ const code = `function foo {
457
+ input {
458
+ }
459
+
460
+ stack {
461
+ }
462
+
463
+ response = null
464
+ }
465
+ ---
466
+ query test verb=GET {
467
+ stack {
468
+ var $result {
469
+ value = "hello"
470
+ }
471
+ }
472
+ }`;
473
+
474
+ const document = TextDocument.create(
475
+ "file://test-multidoc.xs",
476
+ "xanoscript",
477
+ 1,
478
+ code
479
+ );
480
+ const params = {
481
+ textDocument: { uri: "file://test-multidoc.xs" },
482
+ position: { line: 12, character: 4 }, // Position on "var" in second segment
483
+ };
484
+
485
+ const result = onHoverDocument(
486
+ params,
487
+ { get: () => document },
488
+ hoverProviders
489
+ );
490
+
491
+ expect(result).to.not.be.null;
492
+ expect(result.contents.value).to.include("Create a variable");
493
+ expect(result).to.have.property("range");
494
+ // Range should be in the global document, not segment-local
495
+ expect(result.range.start.line).to.be.at.least(10);
496
+ });
497
+
498
+ it("should return hover info for input variable in multidoc segment", () => {
499
+ const code = `function foo {
500
+ input {
501
+ }
502
+
503
+ stack {
504
+ }
505
+
506
+ response = null
507
+ }
508
+ ---
509
+ query test verb=GET {
510
+ input {
511
+ text username
512
+ }
513
+
514
+ stack {
515
+ var $result {
516
+ value = $input.username
517
+ }
518
+ }
519
+
520
+ response = $result
521
+ }`;
522
+
523
+ const document = TextDocument.create(
524
+ "file://test-multidoc-input.xs",
525
+ "xanoscript",
526
+ 1,
527
+ code
528
+ );
529
+ // "username" after $input. in the second segment
530
+ const params = {
531
+ textDocument: { uri: "file://test-multidoc-input.xs" },
532
+ position: { line: 17, character: 20 },
533
+ };
534
+
535
+ const result = onHoverDocument(
536
+ params,
537
+ { get: () => document },
538
+ hoverProviders
539
+ );
540
+
541
+ if (result) {
542
+ expect(result.contents.value).to.include("username");
543
+ expect(result.range.start.line).to.be.at.least(10);
544
+ }
545
+ });
546
+
547
+ it("should return hover info for first segment of multidoc", () => {
548
+ const code = `query test verb=GET {
549
+ stack {
550
+ var $result {
551
+ value = "hello"
552
+ }
553
+ }
554
+ }
555
+ ---
556
+ function bar {
557
+ input {
558
+ }
559
+
560
+ stack {
561
+ }
562
+
563
+ response = null
564
+ }`;
565
+
566
+ const document = TextDocument.create(
567
+ "file://test-multidoc-first.xs",
568
+ "xanoscript",
569
+ 1,
570
+ code
571
+ );
572
+ const params = {
573
+ textDocument: { uri: "file://test-multidoc-first.xs" },
574
+ position: { line: 2, character: 4 }, // "var" in first segment
575
+ };
576
+
577
+ const result = onHoverDocument(
578
+ params,
579
+ { get: () => document },
580
+ hoverProviders
581
+ );
582
+
583
+ expect(result).to.not.be.null;
584
+ expect(result.contents.value).to.include("Create a variable");
585
+ expect(result.range.start.line).to.equal(2);
586
+ });
587
+
588
+ it("should return null for hover on separator line in multidoc", () => {
589
+ const code = `function foo {
590
+ input {
591
+ }
592
+
593
+ stack {
594
+ }
595
+
596
+ response = null
597
+ }
598
+ ---
599
+ function bar {
600
+ input {
601
+ }
602
+
603
+ stack {
604
+ }
605
+
606
+ response = null
607
+ }`;
608
+
609
+ const document = TextDocument.create(
610
+ "file://test-multidoc-sep.xs",
611
+ "xanoscript",
612
+ 1,
613
+ code
614
+ );
615
+ const params = {
616
+ textDocument: { uri: "file://test-multidoc-sep.xs" },
617
+ position: { line: 9, character: 1 }, // On the "---" separator line
618
+ };
619
+
620
+ const result = onHoverDocument(
621
+ params,
622
+ { get: () => document },
623
+ hoverProviders
624
+ );
625
+
626
+ expect(result).to.be.null;
627
+ });
628
+
455
629
  it("should return hover info for query filter tokens in search context", () => {
456
630
  const code = `query books verb=GET {
457
631
  where = $db.embedding.vector|inner_product:$input.query_vector
@@ -10,10 +10,51 @@ import { encodeTokenType } from "./tokens.js";
10
10
  * @returns {SemanticTokensBuilder} Returns null if the scheme is not supported
11
11
  */
12
12
  export function higlightText(scheme, text, SemanticTokensBuilder) {
13
- return higlightDefault(text, SemanticTokensBuilder);
13
+ return higlightDefault(text, 0, SemanticTokensBuilder);
14
14
  }
15
15
 
16
- function higlightDefault(text, SemanticTokensBuilder) {
16
+ /**
17
+ * Highlight a multidoc by highlighting each segment independently.
18
+ * @param {Array<{text: string, globalOffset: number}>} segments - The segments from multidocParser
19
+ * @param {SemanticTokensBuilder} SemanticTokensBuilder - The semantic tokens builder constructor
20
+ */
21
+ /**
22
+ * Highlight a multidoc by highlighting each segment independently.
23
+ * @param {Array<{text: string, globalOffset: number}>} segments - The segments from multidocParser
24
+ * @param {SemanticTokensBuilder} SemanticTokensBuilder - The semantic tokens builder constructor
25
+ */
26
+ export function higlightSegments(segments, SemanticTokensBuilder) {
27
+ const builder = new SemanticTokensBuilder();
28
+
29
+ // Pre-compute line offsets: count newlines in each previous segment + separator
30
+ let lineOffset = 0;
31
+
32
+ for (const segment of segments) {
33
+ const lexResult = lexDocument(segment.text, true);
34
+
35
+ for (const token of lexResult.tokens) {
36
+ const tokenType = mapTokenToType(token.tokenType.name);
37
+ if (tokenType) {
38
+ builder.push(
39
+ token.startLine - 1 + lineOffset,
40
+ token.startColumn - 1,
41
+ token.image.length,
42
+ encodeTokenType(tokenType),
43
+ 0,
44
+ );
45
+ }
46
+ }
47
+
48
+ // Advance lineOffset: lines in this segment's text + 2 for the \n---\n separator
49
+ // (separator has 2 newlines: one before --- and one after)
50
+ const segmentLines = segment.text.split("\n").length - 1;
51
+ lineOffset += segmentLines + 2;
52
+ }
53
+
54
+ return builder.build();
55
+ }
56
+
57
+ function higlightDefault(text, lineOffset, SemanticTokensBuilder) {
17
58
  const builder = new SemanticTokensBuilder();
18
59
 
19
60
  // Map Chevrotain tokens to semantic token types
@@ -23,8 +64,8 @@ function higlightDefault(text, SemanticTokensBuilder) {
23
64
  lexResult.tokens.forEach((token) => {
24
65
  const tokenType = mapTokenToType(token.tokenType.name);
25
66
  if (tokenType) {
26
- const line = token.startLine - 1; // Convert to 0-based for LSP
27
- const character = token.startColumn - 1; // Convert to 0-based for LSP
67
+ const line = token.startLine - 1 + lineOffset;
68
+ const character = token.startColumn - 1;
28
69
  builder.push(
29
70
  line,
30
71
  character,
@@ -1,5 +1,6 @@
1
- import { getSchemeFromContent } from "../utils";
2
- import { higlightText } from "./highlight";
1
+ import { documentCache } from "../cache/documentCache.js";
2
+ import { getSchemeFromContent } from "../utils.js";
3
+ import { higlightSegments,higlightText } from "./highlight.js";
3
4
 
4
5
  /**
5
6
  * Handles a semantic tokens request for the full document.
@@ -19,7 +20,17 @@ export function onSemanticCheck(params, documents, SemanticTokensBuilder) {
19
20
  }
20
21
 
21
22
  const text = document.getText();
22
- const scheme = getSchemeFromContent(text);
23
23
 
24
+ const { parser } = documentCache.getOrParse(
25
+ params.textDocument.uri,
26
+ document.version,
27
+ text,
28
+ );
29
+
30
+ if (parser.isMultidoc) {
31
+ return higlightSegments(parser.segments, SemanticTokensBuilder);
32
+ }
33
+
34
+ const scheme = getSchemeFromContent(text);
24
35
  return higlightText(scheme, text, SemanticTokensBuilder);
25
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xano/xanoscript-language-server",
3
- "version": "11.9.1",
3
+ "version": "11.10.1",
4
4
  "description": "Language Server Protocol implementation for XanoScript",
5
5
  "type": "module",
6
6
  "main": "server.js",