stan-language-server 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -10,28 +10,80 @@ A language server for the Stan probabilistic programming language written in Typ
10
10
  - **Code formatting**: Using the official Stan compiler
11
11
  - **Include file support**: Full `#include` resolution and compilation
12
12
 
13
- To install dependencies:
14
13
 
15
- ```bash
16
- bun install
17
- ```
14
+ ## Editor-specific configuration
18
15
 
19
- To run the language server:
16
+ ### VSCode:
20
17
 
21
- ```bash
22
- bun run src/server.ts
23
- ```
18
+ Install the [extension](https://github.com/WardBrian/vscode-stan-extension)
19
+ from [Marketplace](https://marketplace.visualstudio.com/items?itemName=wardbrian.vscode-stan-extension)
20
+ or [open-vsx](https://open-vsx.org/extension/wardbrian/vscode-stan-extension).
24
21
 
25
- Building a binary executable:
22
+ ### Neovim
26
23
 
27
- ```bash
28
- bun build server.ts --compile --outfile stan-language-server
24
+ There are many ways to install language servers in neovim. Here is one (if you have a different or better way, consider contributing it!):
25
+
26
+ Download the latest language server executable from [GitHub](https://github.com/tomatitito/stan-language-server/tags) and put it somewhere in your PATH. Then in your `nvim` config folder add `lua/lsp/init.lua` with:
27
+ ```lua
28
+ local servers = {
29
+ stan_ls = "lsp.stan",
30
+ }
31
+
32
+ local function setup_server(name, config_module)
33
+ local config = require(config_module)
34
+
35
+ vim.api.nvim_create_autocmd("FileType", {
36
+ pattern = config.filetypes,
37
+ callback = function()
38
+ if #vim.lsp.get_clients({ bufnr = 0, name = name }) > 0 then
39
+ return
40
+ end
41
+
42
+ local root_dir = vim.fs.root(0, config.root_markers)
43
+ print(string.format("Starting %s for buffer %d with root: %s", name, vim.api.nvim_get_current_buf(),
44
+ root_dir or "none"))
45
+
46
+ vim.lsp.start({
47
+ name = name,
48
+ cmd = config.cmd,
49
+ root_dir = root_dir,
50
+ initialization_options = config.settings or {},
51
+ on_exit = function(code, signal)
52
+ print(string.format("%s exited with code %d, signal %d", name, code, signal))
53
+ end,
54
+ })
55
+ end,
56
+ })
57
+ end
58
+
59
+ for server_name, config_path in pairs(servers) do
60
+ setup_server(server_name, config_path)
61
+ end
62
+ ```
63
+
64
+ Then in `lua/lsp/stan.lua` add the following:
65
+ ```lua
66
+ return {
67
+ cmd = { "stan-language-server", "--stdio" },
68
+ filetypes = { "stan" },
69
+ root_markers = { ".git" },
70
+ settings = {
71
+ maxLineLength = 78,
72
+ includePaths = {},
73
+ },
74
+ }
29
75
  ```
30
76
 
31
- ## Configuration
77
+ ### Zed
78
+
79
+ Install the [Stan extension](https://zed.dev/extensions/stan).
32
80
 
33
81
  ### Sublime Text 4
34
82
 
83
+ Using [LSP for Sublime Text](https://lsp.sublimetext.io/),
84
+ [download the latest release](https://github.com/tomatitito/stan-language-server/releases)
85
+ and add the following to the settings file:
86
+
35
87
  ```json
36
88
  {
37
89
  "clients": {
@@ -50,10 +102,41 @@ bun build server.ts --compile --outfile stan-language-server
50
102
 
51
103
  ### Emacs (eglot)
52
104
 
53
- Assuming you are using stan-ts-mode:
105
+ Assuming you are using [stan-ts-mode](https://github.com/WardBrian/stan-ts-mode),
106
+ [download the latest release](https://github.com/tomatitito/stan-language-server/releases)
107
+ and add the following to your `init.el`:
54
108
 
55
109
  ```elisp
110
+ ; elgot is built in to emacs 29+, but a similar config would work for lsp-mode
56
111
  (with-eval-after-load 'eglot
57
112
  (add-to-list 'eglot-server-programs
58
113
  '(stan-ts-mode . ("/YOUR/PATH/TO/stan-language-server" "--stdio"))))
59
114
  ```
115
+
116
+
117
+ ## For developers
118
+
119
+ Development uses [bun](https://bun.sh/)
120
+
121
+ To install dependencies:
122
+
123
+ ```bash
124
+ bun install
125
+ ```
126
+
127
+ Building a binary executable:
128
+
129
+ ```bash
130
+ bun build:binary
131
+ ```
132
+
133
+ To run unit tests:
134
+ ```bash
135
+ bun test
136
+ ```
137
+
138
+ To run end-to-end tests:
139
+ ```bash
140
+ pip install pytest pytest-lsp
141
+ bun build:binary && pytest tests/
142
+ ```
@@ -1,6 +1,7 @@
1
1
  import type { TextDocument } from "vscode-languageserver-textdocument";
2
2
  import { type TextDocuments, type WorkspaceFolder, type RemoteConsole } from "vscode-languageserver";
3
- import type { FileSystemReader, StancReturn } from "../../types/common";
3
+ import type { FileSystemReader } from "../../types/common";
4
+ import { type StancReturn } from "stanc3";
4
5
  export interface Settings {
5
6
  maxLineLength: number;
6
7
  includePaths: string[];
@@ -1,5 +1,5 @@
1
1
  import { TextDocuments, WorkspaceFolder, type RemoteConsole } from "vscode-languageserver";
2
- import type { TextDocument } from "vscode-languageserver-textdocument";
2
+ import { TextDocument } from "vscode-languageserver-textdocument";
3
3
  import type { FileSystemReader } from "../../types";
4
4
  export type Filename = string;
5
5
  export type FileContent = string;
@@ -8,4 +8,4 @@ export type FilePathError = {
8
8
  };
9
9
  export declare function getFilenames(fileContent: string): Filename[];
10
10
  export declare function isFilePathError(value: unknown): value is FilePathError;
11
- export declare function handleIncludes(document: TextDocument, documentManager: TextDocuments<TextDocument>, workspaceFolders: WorkspaceFolder[], includePaths: string[], logger: RemoteConsole, reader?: FileSystemReader): Promise<Record<Filename, FileContent>>;
11
+ export declare function handleIncludes(document: TextDocument, documentManager: TextDocuments<TextDocument>, workspaceFolders: WorkspaceFolder[], includePaths: string[], logger: RemoteConsole, reader?: FileSystemReader, alreadyIncluded?: Set<Filename>): Promise<Record<Filename, FileContent>>;
@@ -1,5 +1,5 @@
1
1
  import { Diagnostic, TextDocuments, WorkspaceFolder, type DocumentDiagnosticParams, type RemoteConsole } from "vscode-languageserver";
2
2
  import { TextDocument } from "vscode-languageserver-textdocument";
3
- import { type Settings } from "./compilation/compilation";
4
3
  import type { FileSystemReader } from "../types";
4
+ import { type Settings } from "./compilation/compilation";
5
5
  export declare function handleDiagnostics(params: DocumentDiagnosticParams, documents: TextDocuments<TextDocument>, workspaceFolders: WorkspaceFolder[], settings: Settings, logger: RemoteConsole, reader?: FileSystemReader): Promise<Diagnostic[]>;
@@ -1,8 +1,5 @@
1
- import type { HoverParams, Hover, MarkupContent } from "vscode-languageserver";
1
+ import type { HoverParams, Hover } from "vscode-languageserver";
2
2
  import type { TextDocument } from "vscode-languageserver-textdocument";
3
3
  export declare const manual_functions: string[];
4
- type MarkupLookupMap = Map<string, MarkupContent>;
5
- type GetMarkupLookupMapFn = () => MarkupLookupMap;
6
- export declare const handleHover: (getMarkupLookupFn: GetMarkupLookupMapFn) => (document: TextDocument, params: HoverParams) => Promise<Hover | null>;
7
- declare const _default: (document: TextDocument, params: HoverParams) => Promise<Hover | null>;
8
- export default _default;
4
+ declare function handleHover(document: TextDocument, params: HoverParams): Promise<Hover | null>;
5
+ export default handleHover;
@@ -1,3 +1,7 @@
1
- import { type StanDiagnostic } from "../../types/diagnostics";
2
- import type { StancReturn } from "../../types/common";
3
- export declare function provideDiagnostics(compilerResult: StancReturn): StanDiagnostic[];
1
+ import type { StancReturn } from "stanc3";
2
+ import type { Range } from "vscode-languageserver";
3
+ export declare function provideDiagnostics(compilerResult: StancReturn): {
4
+ range: Range;
5
+ severity: string;
6
+ message: string;
7
+ }[];
@@ -1,9 +1,10 @@
1
1
  // src/server/index.ts
2
- import { TextDocument } from "vscode-languageserver-textdocument";
2
+ import { TextDocument as TextDocument2 } from "vscode-languageserver-textdocument";
3
3
  import {
4
4
  DiagnosticRefreshRequest,
5
5
  DidChangeConfigurationNotification,
6
6
  DocumentDiagnosticRequest,
7
+ MessageType,
7
8
  TextDocumentSyncKind,
8
9
  TextDocuments as TextDocuments3
9
10
  } from "vscode-languageserver/node";
@@ -217,6 +218,7 @@ var setupDistributionMap = () => {
217
218
  }
218
219
  return distributionToFunctionMap;
219
220
  };
221
+ var DISTRIBUTION_FUNCTION_MAP = setupDistributionMap();
220
222
  var tildeBefore = (text, pos) => {
221
223
  for (let i = pos - 1;i >= 0; i--) {
222
224
  const char = text[i];
@@ -233,9 +235,8 @@ var tildeBefore = (text, pos) => {
233
235
  var tryDistributionHover = (text, beginningOfWord, endOfWord) => {
234
236
  if (!tildeBefore(text, beginningOfWord))
235
237
  return null;
236
- const distributionToFunctionMap = setupDistributionMap();
237
238
  const dist = text.substring(beginningOfWord, endOfWord).trim();
238
- const functionName = distributionToFunctionMap.get(dist);
239
+ const functionName = DISTRIBUTION_FUNCTION_MAP.get(dist);
239
240
  if (!functionName)
240
241
  return null;
241
242
  return functionName;
@@ -325,6 +326,7 @@ var initializeFunctionMarkupMap = () => {
325
326
  }
326
327
  return markupLookupMap;
327
328
  };
329
+ var FUNCTION_MARKUP_LOOKUP = initializeFunctionMarkupMap();
328
330
  function markupContentToHover(content, document, beginningOfWord, endOfWord) {
329
331
  return {
330
332
  contents: content,
@@ -334,15 +336,7 @@ function markupContentToHover(content, document, beginningOfWord, endOfWord) {
334
336
  }
335
337
  };
336
338
  }
337
- var handleHover = (getMarkupLookupFn) => async (document, params) => {
338
- const functionMarkupLookup = getMarkupLookupFn();
339
- const currentLine = document.getText({
340
- start: { line: params.position.line, character: 0 },
341
- end: { line: params.position.line + 1, character: 0 }
342
- }).trim();
343
- if (!currentLine || !currentLine.includes("(")) {
344
- return null;
345
- }
339
+ async function handleHover(document, params) {
346
340
  const text = document.getText();
347
341
  const offset = document.offsetAt(params.position);
348
342
  if (!isWordChar(text[offset])) {
@@ -355,14 +349,14 @@ var handleHover = (getMarkupLookupFn) => async (document, params) => {
355
349
  const beginningOfWord = previousWordBoundary(text, offset);
356
350
  const hoverName = provideHover(text, beginningOfWord, nextParen);
357
351
  if (hoverName) {
358
- const hoverContent = functionMarkupLookup.get(hoverName);
352
+ const hoverContent = FUNCTION_MARKUP_LOOKUP.get(hoverName);
359
353
  if (hoverContent !== undefined) {
360
354
  return markupContentToHover(hoverContent, document, beginningOfWord, nextParen);
361
355
  }
362
356
  }
363
357
  return null;
364
- };
365
- var hover_default = handleHover(initializeFunctionMarkupMap);
358
+ }
359
+ var hover_default = handleHover;
366
360
 
367
361
  // src/language/completion/providers/functions.ts
368
362
  var provideFunctionCompletions = (text, position, functionSignatures) => {
@@ -457,31 +451,21 @@ function constraintToCompletionItem(constraint) {
457
451
  kind: CompletionItemKind.Property
458
452
  };
459
453
  }
460
- function convertPosition(position) {
461
- return {
462
- line: position.line,
463
- character: position.character
464
- };
465
- }
466
- function getDistributionData() {
467
- return dump_stan_math_distributions2().split(`
454
+ var DISTRIBUTION_DATA = dump_stan_math_distributions2().split(`
468
455
  `).map((line) => line.split(":")[0]?.trim() ?? "").filter((name) => name !== "");
469
- }
470
- function getFunctionData() {
471
- return dump_stan_math_signatures2().split(`
456
+ var FUNCTION_DATA = dump_stan_math_signatures2().split(`
472
457
  `);
473
- }
474
458
  function handleCompletion(params, documents) {
475
459
  const document = documents.get(params.textDocument.uri);
476
460
  if (!document) {
477
461
  return [];
478
462
  }
479
463
  const text = document.getText();
480
- const position = convertPosition(params.position);
464
+ const position = params.position;
481
465
  const keywords = provideKeywordCompletions(text, position);
482
- const distributions = provideDistributionCompletions(text, position, getDistributionData());
466
+ const distributions = provideDistributionCompletions(text, position, DISTRIBUTION_DATA);
483
467
  const datatypes = provideDatatypeCompletions(text, position);
484
- const functions = provideFunctionCompletions(text, position, getFunctionData());
468
+ const functions = provideFunctionCompletions(text, position, FUNCTION_DATA);
485
469
  const constraints = provideConstraintCompletions(text, position);
486
470
  const allItems = [
487
471
  ...keywords.map(keywordToCompletionItem),
@@ -494,14 +478,18 @@ function handleCompletion(params, documents) {
494
478
  }
495
479
  // src/handlers/diagnostics.ts
496
480
  import {
497
- DiagnosticSeverity as DiagnosticSeverity2
481
+ DiagnosticSeverity
498
482
  } from "vscode-languageserver";
499
483
 
500
- // src/language/diagnostics/linter.ts
501
- function rangeFromMessage(message) {
502
- if (!message)
484
+ // src/constants/index.ts
485
+ var SERVER_ID = "stan-language-server";
486
+
487
+ // src/language/diagnostics/provider.ts
488
+ function getRangeFromMessage(message) {
489
+ const msg = String(message);
490
+ if (!msg)
503
491
  return;
504
- const start = message.matchAll(/'.*', line (\d+), column (\d+)( to)?/g);
492
+ const start = msg.matchAll(/'.*', line (\d+), column (\d+)( to)?/g);
505
493
  const lastMatch = Array.from(start).pop();
506
494
  if (!lastMatch || !lastMatch[1] || !lastMatch[2]) {
507
495
  return;
@@ -511,7 +499,7 @@ function rangeFromMessage(message) {
511
499
  let endLine = startLine;
512
500
  let endColumn = startColumn;
513
501
  if (lastMatch[3]) {
514
- const end = message.match(/to (line (\d+), )?column (\d+)/);
502
+ const end = msg.match(/to (line (\d+), )?column (\d+)/);
515
503
  if (end && end[3]) {
516
504
  if (end[1] && end[2]) {
517
505
  endLine = parseInt(end[2]) - 1;
@@ -525,64 +513,60 @@ function rangeFromMessage(message) {
525
513
  };
526
514
  }
527
515
  function getWarningMessage(message) {
528
- let warning = message.replace(/Warning.*column \d+: /s, "");
516
+ const msg = String(message);
517
+ let warning = msg.replace(/Warning.*column \d+: /s, "");
529
518
  warning = warning.replace(/\s+/gs, " ");
530
519
  warning = warning.trim();
531
- warning = message.includes("included from") ? `Warning in included file:
520
+ warning = msg.includes("included from") ? `Warning in included file:
532
521
  ` + warning : warning;
533
522
  return warning;
534
523
  }
535
524
  function getErrorMessage(message) {
536
- let error = message;
537
- if (message.includes(`------
525
+ const msg = String(message);
526
+ let error = msg;
527
+ if (msg.includes(`------
538
528
  `)) {
539
529
  error = error.split(`------
540
530
  `)[2] ?? error;
541
531
  }
542
532
  error = error.trim();
543
- error = message.includes("included from") ? `Error in included file:
533
+ error = msg.includes("included from") ? `Error in included file:
544
534
  ` + error : error;
545
535
  error = error.includes("given information about") ? error + `
546
536
  Try opening the included file and making the Stan language server aware of it.` : error;
547
537
  return error;
548
538
  }
549
-
550
- // src/constants/index.ts
551
- var SERVER_ID = "stan-language-server";
552
-
553
- // src/language/diagnostics/provider.ts
539
+ function provideErrorMessageAndRange(message) {
540
+ return { range: getRangeFromMessage(message), message: getErrorMessage(message) };
541
+ }
542
+ function provideWarningMessageAndRange(message) {
543
+ return { range: getRangeFromMessage(message), message: getWarningMessage(message) };
544
+ }
554
545
  function provideDiagnostics(compilerResult) {
555
- const diagnostics = [];
556
- if (compilerResult.errors) {
557
- for (const error of compilerResult.errors) {
558
- const range = rangeFromMessage(error);
559
- if (range) {
560
- diagnostics.push({
561
- range,
562
- severity: 1 /* Error */,
563
- message: getErrorMessage(error),
564
- source: SERVER_ID
565
- });
566
- }
567
- }
568
- }
569
- if (compilerResult.warnings) {
570
- for (const warning of compilerResult.warnings) {
571
- const range = rangeFromMessage(warning);
572
- if (range) {
573
- diagnostics.push({
574
- range,
575
- severity: 2 /* Warning */,
576
- message: getWarningMessage(warning),
577
- source: SERVER_ID
578
- });
579
- }
580
- }
581
- }
582
- return diagnostics;
546
+ const errorDiagnostics = compilerResult.errors?.map((msg) => {
547
+ return provideErrorMessageAndRange(msg);
548
+ }).filter((item) => item.range !== undefined && item.message !== undefined).map(({ range, message }) => {
549
+ return {
550
+ range,
551
+ severity: "error",
552
+ message
553
+ };
554
+ }) ?? [];
555
+ const warningDiagnostics = compilerResult.warnings?.map((msg) => {
556
+ return provideWarningMessageAndRange(msg);
557
+ }).filter((item) => item.range !== undefined).map(({ range, message }) => {
558
+ return {
559
+ range,
560
+ severity: "warning",
561
+ message
562
+ };
563
+ }) ?? [];
564
+ return [...errorDiagnostics, ...warningDiagnostics];
583
565
  }
566
+
584
567
  // src/handlers/compilation/includes.ts
585
568
  import { join } from "path";
569
+ import { TextDocument } from "vscode-languageserver-textdocument";
586
570
  import { URI, Utils } from "vscode-uri";
587
571
  function getFilenames(fileContent) {
588
572
  const includePattern = /#include\s*[<"]?([^>"\s]*)[>"]?/g;
@@ -593,13 +577,16 @@ function getFilenames(fileContent) {
593
577
  function isFilePathError(value) {
594
578
  return typeof value === "object" && value !== null && "msg" in value;
595
579
  }
596
- async function handleIncludes(document, documentManager, workspaceFolders, includePaths, logger, reader) {
580
+ async function handleIncludes(document, documentManager, workspaceFolders, includePaths, logger, reader, alreadyIncluded = new Set) {
597
581
  try {
598
582
  const includeFilenames = getFilenames(document.getText());
599
583
  if (includeFilenames.length === 0) {
600
584
  return {};
601
585
  }
602
586
  const allResults = await Promise.all(includeFilenames.map(async (filename) => {
587
+ if (alreadyIncluded.has(filename)) {
588
+ return [filename, { msg: `File already included: ${filename}` }];
589
+ }
603
590
  try {
604
591
  const content = await readIncludedFile(document, documentManager, workspaceFolders, includePaths, filename, reader);
605
592
  return [filename, content];
@@ -608,7 +595,10 @@ async function handleIncludes(document, documentManager, workspaceFolders, inclu
608
595
  }
609
596
  }));
610
597
  const validResults = allResults.filter(([_, content]) => !isFilePathError(content));
611
- return Object.fromEntries(validResults);
598
+ const currentlyIncluded = new Set(validResults.map(([filename, _]) => filename)).union(alreadyIncluded);
599
+ const recursiveIncludes = await Promise.all(validResults.map(async ([_, content]) => await handleIncludes(content, documentManager, workspaceFolders, includePaths, logger, reader, currentlyIncluded)));
600
+ const results = validResults.map(([filename, contents]) => [filename, contents.getText()]).concat(recursiveIncludes.map(Object.entries).flat());
601
+ return Object.fromEntries(results);
612
602
  } catch (error) {
613
603
  logger.warn(`Resolving included files failed: ${error}`);
614
604
  return Promise.resolve({});
@@ -621,10 +611,7 @@ var readIncludedFile = async (document, documentManager, workspaceFolders, inclu
621
611
  return Promise.resolve(includedFileContent);
622
612
  }
623
613
  if (reader) {
624
- includedFileContent = await readIncludedFileFromFileSystem(filename, [
625
- currentDir.fsPath,
626
- ...includePaths
627
- ], reader);
614
+ includedFileContent = await readIncludedFileFromFileSystem(filename, [currentDir.fsPath, ...includePaths], reader);
628
615
  }
629
616
  if (!isFilePathError(includedFileContent)) {
630
617
  return Promise.resolve(includedFileContent);
@@ -645,13 +632,14 @@ var readIncludedFileFromWorkspace = (documentManager, workspaceFolders, filename
645
632
  if (!includedFile) {
646
633
  return Promise.resolve({ msg: `File not found: ${filename}` });
647
634
  }
648
- return Promise.resolve(includedFile.getText());
635
+ return Promise.resolve(includedFile);
649
636
  };
650
637
  var readIncludedFileFromFileSystem = async (filename, dirs, fileSystemReader) => {
651
638
  for (const currentDir of dirs) {
652
639
  try {
653
640
  const localPath = join(currentDir, filename);
654
- return await fileSystemReader(localPath);
641
+ const content = await fileSystemReader(localPath);
642
+ return TextDocument.create(URI.file(localPath).toString(), "stan", 0, content);
655
643
  } catch (error) {}
656
644
  }
657
645
  return Promise.resolve({ msg: `File not found: ${filename}` });
@@ -682,48 +670,30 @@ async function handleCompilation(document, documentManager, workspaceFolders, se
682
670
  }
683
671
 
684
672
  // src/handlers/diagnostics.ts
685
- function stanDiagnosticToLspDiagnostic(stanDiag) {
686
- return {
687
- range: domainRangeToLspRange(stanDiag.range),
688
- severity: domainSeverityToLspSeverity(stanDiag.severity),
689
- message: stanDiag.message,
690
- source: stanDiag.source ?? SERVER_ID
691
- };
692
- }
693
- function domainRangeToLspRange(domainRange) {
694
- return {
695
- start: {
696
- line: domainRange.start.line,
697
- character: domainRange.start.character
698
- },
699
- end: {
700
- line: domainRange.end.line,
701
- character: domainRange.end.character
702
- }
703
- };
704
- }
705
- function domainSeverityToLspSeverity(domainSeverity) {
706
- switch (domainSeverity) {
707
- case 1:
708
- return DiagnosticSeverity2.Error;
709
- case 2:
710
- return DiagnosticSeverity2.Warning;
711
- case 3:
712
- return DiagnosticSeverity2.Information;
713
- case 4:
714
- return DiagnosticSeverity2.Hint;
715
- default:
716
- return DiagnosticSeverity2.Error;
717
- }
718
- }
719
673
  async function handleDiagnostics(params, documents, workspaceFolders, settings, logger, reader) {
720
674
  const document = documents.get(params.textDocument.uri);
721
675
  if (!document) {
722
676
  return [];
723
677
  }
724
678
  const compilerResult = await handleCompilation(document, documents, workspaceFolders, settings, logger, reader);
725
- const stanDiagnostics = provideDiagnostics(compilerResult);
726
- return stanDiagnostics.map(stanDiagnosticToLspDiagnostic);
679
+ const diagnostics = provideDiagnostics(compilerResult).map((diagnostic) => {
680
+ if (diagnostic.severity === "error") {
681
+ return {
682
+ range: diagnostic.range,
683
+ severity: DiagnosticSeverity.Error,
684
+ message: diagnostic.message,
685
+ source: SERVER_ID
686
+ };
687
+ } else if (diagnostic.severity === "warning") {
688
+ return {
689
+ range: diagnostic.range,
690
+ severity: DiagnosticSeverity.Warning,
691
+ message: diagnostic.message,
692
+ source: SERVER_ID
693
+ };
694
+ }
695
+ }).filter((diagnostic) => diagnostic !== undefined);
696
+ return diagnostics;
727
697
  }
728
698
  // src/handlers/formatting.ts
729
699
  async function handleFormatting(params, documents, workspaceFolders, settings, logger, reader) {
@@ -755,11 +725,13 @@ async function handleFormatting(params, documents, workspaceFolders, settings, l
755
725
  var startLanguageServer = (connection, reader) => {
756
726
  let hasConfigurationCapability = false;
757
727
  let hasWorkspaceFolderCapability = false;
728
+ let hasDynamicConfigurationRequestCapability = false;
758
729
  connection.onInitialize((params) => {
759
730
  connection.console.info("Initializing Stan language server...");
760
731
  let capabilities = params.capabilities;
761
- hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration);
762
- hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);
732
+ hasConfigurationCapability = Boolean(capabilities.workspace?.configuration);
733
+ hasDynamicConfigurationRequestCapability = Boolean(capabilities.workspace?.configuration && capabilities.workspace?.didChangeConfiguration?.dynamicRegistration);
734
+ hasWorkspaceFolderCapability = Boolean(capabilities.workspace?.workspaceFolders);
763
735
  return {
764
736
  capabilities: {
765
737
  textDocumentSync: TextDocumentSyncKind.Incremental,
@@ -768,6 +740,7 @@ var startLanguageServer = (connection, reader) => {
768
740
  resolveProvider: false
769
741
  },
770
742
  documentFormattingProvider: true,
743
+ documentRangeFormattingProvider: false,
771
744
  workspace: {
772
745
  workspaceFolders: {
773
746
  supported: hasWorkspaceFolderCapability
@@ -781,9 +754,10 @@ var startLanguageServer = (connection, reader) => {
781
754
  }
782
755
  };
783
756
  });
784
- connection.onInitialized(() => {
785
- if (hasConfigurationCapability) {
786
- connection.client.register(DidChangeConfigurationNotification.type);
757
+ connection.onInitialized(async () => {
758
+ if (hasDynamicConfigurationRequestCapability) {
759
+ await connection.client.register(DidChangeConfigurationNotification.type);
760
+ connection.console.info("Registered for didChangeConfiguration");
787
761
  }
788
762
  connection.console.info("Stan language server is initialized!");
789
763
  });
@@ -793,7 +767,7 @@ var startLanguageServer = (connection, reader) => {
793
767
  let globalSettings = defaultSettings;
794
768
  let documentSettings = new Map;
795
769
  connection.onDidChangeConfiguration((change) => {
796
- if (hasConfigurationCapability) {
770
+ if (hasDynamicConfigurationRequestCapability) {
797
771
  documentSettings.clear();
798
772
  } else {
799
773
  const incomingSettings = change.settings[SERVER_ID] || {};
@@ -817,7 +791,7 @@ var startLanguageServer = (connection, reader) => {
817
791
  documentSettings.set(resource, docSettings);
818
792
  return docSettings;
819
793
  };
820
- const documents = new TextDocuments3(TextDocument);
794
+ const documents = new TextDocuments3(TextDocument2);
821
795
  connection.onCompletion((params) => {
822
796
  return handleCompletion(params, documents);
823
797
  });
@@ -846,6 +820,10 @@ var startLanguageServer = (connection, reader) => {
846
820
  for (const error of formattingResult.errors) {
847
821
  connection.console.error(error);
848
822
  }
823
+ connection.sendNotification("window/showMessage", {
824
+ type: MessageType.Error,
825
+ message: "Formatting failed due to compile errors. See diagnostics for details."
826
+ });
849
827
  return [];
850
828
  }
851
829
  });
@@ -3,17 +3,4 @@ export interface Position {
3
3
  line: number;
4
4
  character: number;
5
5
  }
6
- type StancSuccess = {
7
- errors: undefined;
8
- result: string;
9
- warnings?: string[];
10
- };
11
- type StancFailure = {
12
- errors: string[];
13
- result: undefined;
14
- warnings?: string[];
15
- };
16
- export type StancReturn = StancSuccess | StancFailure;
17
- export type StancFunction = (filename: string, code: string, options: string[], includes?: Record<string, string>) => StancReturn;
18
6
  export type FileSystemReader = (filename: Filename) => Promise<FileContent>;
19
- export {};
@@ -1,3 +1,2 @@
1
1
  export * from './common';
2
2
  export * from './completion';
3
- export * from './diagnostics';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stan-language-server",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Language Server Protocol implementation for the Stan probabilistic programming language",
5
5
  "main": "dist/server/index.js",
6
6
  "module": "src/server/index.ts",
@@ -64,11 +64,11 @@
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/bun": "latest",
67
- "@types/node": "24.5.2",
67
+ "@types/node": "24.8.1",
68
68
  "@typescript-eslint/eslint-plugin": "^8.0.0",
69
69
  "@typescript-eslint/parser": "^8.0.0",
70
70
  "eslint": "^9.0.0",
71
- "typescript": "5.9.2"
71
+ "typescript": "5.9.3"
72
72
  },
73
73
  "dependencies": {
74
74
  "stanc3": "^2.37.0",
@@ -1,2 +0,0 @@
1
- export { provideDiagnostics } from "./provider";
2
- export { rangeFromMessage, getWarningMessage, getErrorMessage } from "./linter";
@@ -1,5 +0,0 @@
1
- import type { Range } from "../../types/diagnostics";
2
- declare function rangeFromMessage(message: string): Range | undefined;
3
- declare function getWarningMessage(message: string): string;
4
- declare function getErrorMessage(message: string): string;
5
- export { rangeFromMessage, getWarningMessage, getErrorMessage };
@@ -1,18 +0,0 @@
1
- import type { Position } from "./common";
2
- export type { Position };
3
- export interface Range {
4
- start: Position;
5
- end: Position;
6
- }
7
- export declare enum DiagnosticSeverity {
8
- Error = 1,
9
- Warning = 2,
10
- Information = 3,
11
- Hint = 4
12
- }
13
- export interface StanDiagnostic {
14
- range: Range;
15
- severity: DiagnosticSeverity;
16
- message: string;
17
- source?: string;
18
- }