@rohal12/spindle-lsp 0.3.5 → 0.5.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/dist/bin.js CHANGED
@@ -1011,6 +1011,12 @@ var init_macro_supplements = __esm({
1011
1011
  name: "unwatch",
1012
1012
  description: "Stops watching a variable.\n\nUsage: `{unwatch $var}`",
1013
1013
  parameters: ["...text"]
1014
+ },
1015
+ unless: {
1016
+ name: "unless",
1017
+ container: true,
1018
+ description: "Conditionally displays its contents when the expression is falsy. The inverse of `{if}`.\n\nUsage:\n\n```\n{unless condition}\n ...\n{/unless}\n```",
1019
+ parameters: ["...text"]
1014
1020
  }
1015
1021
  };
1016
1022
  }
@@ -2787,51 +2793,369 @@ var init_check = __esm({
2787
2793
  }
2788
2794
  });
2789
2795
 
2790
- // src/plugins/format.ts
2791
- function formatDocument(text) {
2796
+ // src/plugins/format/segment.ts
2797
+ function splitPassages(text) {
2792
2798
  const lines = text.split("\n");
2793
- const result = [];
2794
- let indentLevel = 0;
2799
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
2800
+ lines.pop();
2801
+ }
2802
+ const passages = [];
2803
+ let currentHeader = "";
2804
+ let bodyLines = [];
2805
+ let startLine = 0;
2806
+ function pushPassage() {
2807
+ if (currentHeader || bodyLines.length > 0) {
2808
+ while (bodyLines.length > 0 && bodyLines[bodyLines.length - 1].trim() === "") {
2809
+ bodyLines.pop();
2810
+ }
2811
+ passages.push({
2812
+ header: currentHeader,
2813
+ body: bodyLines.join("\n") + (bodyLines.length > 0 ? "\n" : ""),
2814
+ startLine
2815
+ });
2816
+ }
2817
+ }
2795
2818
  for (let i = 0; i < lines.length; i++) {
2796
- let line = lines[i];
2797
- line = line.replace(/\s+$/, "");
2798
- if (PASSAGE_HEADER_REGEX.test(line)) {
2799
- line = normalizePassageHeader(line);
2800
- indentLevel = 0;
2801
- result.push(line);
2819
+ if (PASSAGE_HEADER_REGEX.test(lines[i])) {
2820
+ pushPassage();
2821
+ currentHeader = lines[i];
2822
+ bodyLines = [];
2823
+ startLine = i;
2824
+ } else {
2825
+ bodyLines.push(lines[i]);
2826
+ }
2827
+ }
2828
+ pushPassage();
2829
+ return passages;
2830
+ }
2831
+ function classifyPassage(header) {
2832
+ const tagMatch = header.match(/\[([^\]]*)\]/g);
2833
+ if (!tagMatch) return "normal";
2834
+ const tags = tagMatch.map((t) => t.slice(1, -1).trim().toLowerCase());
2835
+ if (tags.includes("script")) return "script";
2836
+ if (tags.includes("stylesheet")) return "stylesheet";
2837
+ return "normal";
2838
+ }
2839
+ function segmentRegions(body) {
2840
+ const lines = body.split("\n");
2841
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
2842
+ lines.pop();
2843
+ }
2844
+ if (lines.length === 0) return [];
2845
+ const regions = [];
2846
+ let i = 0;
2847
+ while (i < lines.length) {
2848
+ const line = lines[i];
2849
+ const trimmed = line.trim();
2850
+ if (/^<script(\s|>)/i.test(trimmed)) {
2851
+ const scriptLines = [line];
2852
+ i++;
2853
+ while (i < lines.length && !/<\/script>/i.test(lines[i])) {
2854
+ scriptLines.push(lines[i]);
2855
+ i++;
2856
+ }
2857
+ if (i < lines.length) {
2858
+ scriptLines.push(lines[i]);
2859
+ i++;
2860
+ }
2861
+ regions.push({ type: "script", lines: scriptLines });
2862
+ continue;
2863
+ }
2864
+ if (/^<([a-zA-Z][\w-]*)/.test(trimmed)) {
2865
+ const tagMatch = trimmed.match(/^<([a-zA-Z][\w-]*)/);
2866
+ const tagName = tagMatch[1].toLowerCase();
2867
+ if (VOID_ELEMENTS.has(tagName) && !lines[i + 1]?.trim().startsWith("<")) {
2868
+ pushSpindleLine(regions, line);
2869
+ i++;
2870
+ continue;
2871
+ }
2872
+ const selfClosing = new RegExp(`</${tagName}\\s*>`, "i");
2873
+ if (selfClosing.test(trimmed)) {
2874
+ pushSpindleLine(regions, line);
2875
+ i++;
2876
+ continue;
2877
+ }
2878
+ const htmlLines = [line];
2879
+ let depth = 1;
2880
+ i++;
2881
+ while (i < lines.length && depth > 0) {
2882
+ htmlLines.push(lines[i]);
2883
+ const opens = (lines[i].match(new RegExp(`<${tagName}[\\s>]`, "gi")) ?? []).length;
2884
+ const closes = (lines[i].match(new RegExp(`</${tagName}\\s*>`, "gi")) ?? []).length;
2885
+ depth += opens - closes;
2886
+ i++;
2887
+ }
2888
+ regions.push({ type: "html", lines: htmlLines });
2802
2889
  continue;
2803
2890
  }
2891
+ pushSpindleLine(regions, line);
2892
+ i++;
2893
+ }
2894
+ return regions;
2895
+ }
2896
+ function pushSpindleLine(regions, line) {
2897
+ const last = regions[regions.length - 1];
2898
+ if (last?.type === "spindle") {
2899
+ last.lines.push(line);
2900
+ } else {
2901
+ regions.push({ type: "spindle", lines: [line] });
2902
+ }
2903
+ }
2904
+ var PASSAGE_HEADER_REGEX, VOID_ELEMENTS;
2905
+ var init_segment = __esm({
2906
+ "src/plugins/format/segment.ts"() {
2907
+ "use strict";
2908
+ PASSAGE_HEADER_REGEX = /^::\s+/;
2909
+ VOID_ELEMENTS = /* @__PURE__ */ new Set([
2910
+ "area",
2911
+ "base",
2912
+ "br",
2913
+ "col",
2914
+ "embed",
2915
+ "hr",
2916
+ "img",
2917
+ "input",
2918
+ "link",
2919
+ "meta",
2920
+ "param",
2921
+ "source",
2922
+ "track",
2923
+ "wbr"
2924
+ ]);
2925
+ }
2926
+ });
2927
+
2928
+ // src/plugins/format/prettier-bridge.ts
2929
+ async function loadPrettier() {
2930
+ if (prettierLoaded) return prettier;
2931
+ prettierLoaded = true;
2932
+ try {
2933
+ prettier = await import("prettier");
2934
+ } catch {
2935
+ prettier = null;
2936
+ }
2937
+ return prettier;
2938
+ }
2939
+ async function getBaseConfig() {
2940
+ const p = await loadPrettier();
2941
+ if (!p) return {};
2942
+ try {
2943
+ const resolved = await p.resolveConfig(process.cwd());
2944
+ return { tabWidth: 2, ...resolved ?? {} };
2945
+ } catch {
2946
+ return { tabWidth: 2 };
2947
+ }
2948
+ }
2949
+ async function formatJS(code) {
2950
+ const p = await loadPrettier();
2951
+ if (!p) return code;
2952
+ try {
2953
+ const config = await getBaseConfig();
2954
+ return await p.format(code, { ...config, parser: "babel" });
2955
+ } catch {
2956
+ return code;
2957
+ }
2958
+ }
2959
+ async function formatCSS(code) {
2960
+ const p = await loadPrettier();
2961
+ if (!p) return code;
2962
+ try {
2963
+ const config = await getBaseConfig();
2964
+ return await p.format(code, { ...config, parser: "css" });
2965
+ } catch {
2966
+ return code;
2967
+ }
2968
+ }
2969
+ async function formatHTML(code) {
2970
+ const p = await loadPrettier();
2971
+ if (!p) return code;
2972
+ try {
2973
+ const config = await getBaseConfig();
2974
+ return await p.format(code, {
2975
+ ...config,
2976
+ parser: "html",
2977
+ htmlWhitespaceSensitivity: "ignore"
2978
+ });
2979
+ } catch {
2980
+ return code;
2981
+ }
2982
+ }
2983
+ var prettier, prettierLoaded;
2984
+ var init_prettier_bridge = __esm({
2985
+ "src/plugins/format/prettier-bridge.ts"() {
2986
+ "use strict";
2987
+ prettier = null;
2988
+ prettierLoaded = false;
2989
+ }
2990
+ });
2991
+
2992
+ // src/plugins/format/placeholders.ts
2993
+ function isInsideAttribute(text, pos) {
2994
+ let inQuote = false;
2995
+ let quoteChar = "";
2996
+ for (let i = 0; i < pos; i++) {
2997
+ const ch = text[i];
2998
+ if (!inQuote && (ch === '"' || ch === "'") && i > 0 && text[i - 1] === "=") {
2999
+ inQuote = true;
3000
+ quoteChar = ch;
3001
+ } else if (inQuote && ch === quoteChar) {
3002
+ inQuote = false;
3003
+ }
3004
+ }
3005
+ return inQuote;
3006
+ }
3007
+ function replaceSpindleTokens(html) {
3008
+ const tokens = [];
3009
+ const text = html.replace(SPINDLE_TOKEN_REGEX, (match, ...args2) => {
3010
+ const offset = args2[args2.length - 2];
3011
+ const idx = tokens.length;
3012
+ tokens.push(match);
3013
+ if (isInsideAttribute(html, offset)) {
3014
+ return `__SP${idx}__`;
3015
+ }
3016
+ return `<!--SP:${idx}-->`;
3017
+ });
3018
+ return { text, tokens };
3019
+ }
3020
+ function restoreSpindleTokens(text, tokens) {
3021
+ let result = text;
3022
+ for (let i = 0; i < tokens.length; i++) {
3023
+ result = result.replace(`<!--SP:${i}-->`, tokens[i]);
3024
+ result = result.replace(`__SP${i}__`, tokens[i]);
3025
+ }
3026
+ return result;
3027
+ }
3028
+ var SPINDLE_TOKEN_REGEX;
3029
+ var init_placeholders = __esm({
3030
+ "src/plugins/format/placeholders.ts"() {
3031
+ "use strict";
3032
+ SPINDLE_TOKEN_REGEX = /(?:\{\/[A-Za-z][\w-]*\s*\}|\{(?:[#.][a-zA-Z][\w-]*\s*)*[A-Za-z][\w-]*(?:\s+(?:[^}]|\}(?=[^}]))*?)?\}|\{[$_@][a-zA-Z][\w.]*\}|\[\[(?:[^\]]*)\]\])/g;
3033
+ }
3034
+ });
3035
+
3036
+ // src/plugins/format.ts
3037
+ function getSupplementContainers() {
3038
+ const containers = /* @__PURE__ */ new Set();
3039
+ for (const [key, entry] of Object.entries(macro_supplements_default)) {
3040
+ if (entry.container) {
3041
+ containers.add(key.toLowerCase());
3042
+ }
3043
+ }
3044
+ return containers;
3045
+ }
3046
+ function detectContainersFromText(text) {
3047
+ const found = /* @__PURE__ */ new Set();
3048
+ const re = /\{\/([A-Za-z][\w-]*)\s*\}/g;
3049
+ let m;
3050
+ while ((m = re.exec(text)) !== null) {
3051
+ found.add(m[1].toLowerCase());
3052
+ }
3053
+ return found;
3054
+ }
3055
+ function buildDefaultIsBlock(text) {
3056
+ const containers = getSupplementContainers();
3057
+ for (const name of detectContainersFromText(text)) {
3058
+ containers.add(name);
3059
+ }
3060
+ return (name) => containers.has(name.toLowerCase());
3061
+ }
3062
+ async function formatDocument(text, options) {
3063
+ const isBlock = options?.isBlock ?? buildDefaultIsBlock(text);
3064
+ const isDedenting = options?.isDedentingSubMacro ?? ((name) => DEFAULT_DEDENTING.has(name.toLowerCase()));
3065
+ const passages = splitPassages(text);
3066
+ const resultLines = [];
3067
+ for (let pi = 0; pi < passages.length; pi++) {
3068
+ const passage = passages[pi];
3069
+ if (pi > 0 && passage.header) {
3070
+ const prev = passages[pi - 1];
3071
+ const prevBodyLineCount = prev.body ? prev.body.split("\n").length - 1 : 0;
3072
+ const prevEndLine = prev.startLine + (prev.header ? 1 : 0) + prevBodyLineCount;
3073
+ if (passage.startLine > prevEndLine) {
3074
+ resultLines.push("");
3075
+ }
3076
+ }
3077
+ if (passage.header) {
3078
+ const header = PASSAGE_HEADER_REGEX2.test(passage.header) ? normalizePassageHeader(passage.header) : passage.header;
3079
+ resultLines.push(header);
3080
+ }
3081
+ const kind = classifyPassage(passage.header);
3082
+ if (kind === "script") {
3083
+ const formatted = await formatJS(passage.body.trim());
3084
+ resultLines.push(formatted.trim());
3085
+ continue;
3086
+ }
3087
+ if (kind === "stylesheet") {
3088
+ const formatted = await formatCSS(passage.body.trim());
3089
+ resultLines.push(formatted.trim());
3090
+ continue;
3091
+ }
3092
+ const regions = segmentRegions(passage.body);
3093
+ for (const region of regions) {
3094
+ if (region.type === "script") {
3095
+ const firstLine = region.lines[0];
3096
+ const lastLine = region.lines[region.lines.length - 1];
3097
+ const innerLines = region.lines.slice(1, region.lines.length - 1);
3098
+ const innerCode = innerLines.join("\n");
3099
+ const formatted = await formatJS(innerCode.trim());
3100
+ resultLines.push(firstLine);
3101
+ if (formatted.trim()) {
3102
+ for (const fLine of formatted.trim().split("\n")) {
3103
+ resultLines.push(" " + fLine);
3104
+ }
3105
+ }
3106
+ resultLines.push(lastLine);
3107
+ continue;
3108
+ }
3109
+ if (region.type === "html") {
3110
+ const htmlText = region.lines.join("\n");
3111
+ const { text: placeholdered, tokens } = replaceSpindleTokens(htmlText);
3112
+ const formatted = await formatHTML(placeholdered);
3113
+ const restored = restoreSpindleTokens(formatted.trim(), tokens);
3114
+ for (const fLine of restored.split("\n")) {
3115
+ resultLines.push(fLine);
3116
+ }
3117
+ continue;
3118
+ }
3119
+ const indented = indentMacros(region.lines, isBlock, isDedenting);
3120
+ resultLines.push(...indented);
3121
+ }
3122
+ }
3123
+ let output = resultLines.map((l) => l.replace(/\s+$/, "")).join("\n");
3124
+ output = output.replace(/\n*$/, "\n");
3125
+ return output;
3126
+ }
3127
+ function indentMacros(lines, isBlock, isDedenting) {
3128
+ const result = [];
3129
+ let indentLevel = 0;
3130
+ for (const rawLine of lines) {
3131
+ const line = rawLine.replace(/\s+$/, "");
2804
3132
  const trimmed = line.trim();
2805
3133
  if (trimmed === "") {
2806
3134
  result.push("");
2807
3135
  continue;
2808
3136
  }
2809
- if (BLOCK_CLOSE_REGEX.test(trimmed)) {
3137
+ const dedentMatch = trimmed.match(MACRO_OPEN_REGEX);
3138
+ if (dedentMatch && isDedenting(dedentMatch[1])) {
3139
+ indentLevel = Math.max(0, indentLevel - 1);
3140
+ result.push(indentLevel > 0 ? " ".repeat(indentLevel) + trimmed : trimmed);
3141
+ indentLevel++;
3142
+ continue;
3143
+ }
3144
+ const closeMatch = trimmed.match(MACRO_CLOSE_REGEX);
3145
+ if (closeMatch) {
2810
3146
  indentLevel = Math.max(0, indentLevel - 1);
2811
3147
  }
2812
- const indented = indentLevel > 0 ? " ".repeat(indentLevel) + trimmed : trimmed;
2813
- result.push(indented);
2814
- if (BLOCK_OPEN_REGEX.test(trimmed)) {
3148
+ result.push(indentLevel > 0 ? " ".repeat(indentLevel) + trimmed : trimmed);
3149
+ const openMatch = trimmed.match(MACRO_OPEN_REGEX);
3150
+ if (openMatch && isBlock(openMatch[1])) {
2815
3151
  indentLevel++;
2816
3152
  }
2817
3153
  }
2818
- let output = result.join("\n");
2819
- output = output.replace(/\n*$/, "\n");
2820
- return output;
2821
- }
2822
- function formatRange(text, range) {
2823
- const lines = text.split("\n");
2824
- const startLine = Math.max(0, range.start.line);
2825
- const endLine = Math.min(lines.length - 1, range.end.line);
2826
- const rangeLines = lines.slice(startLine, endLine + 1);
2827
- const rangeText = rangeLines.join("\n");
2828
- const formatted = formatDocument(rangeText);
2829
- const formattedLines = formatted.replace(/\n$/, "").split("\n");
2830
- const before = lines.slice(0, startLine);
2831
- const after = lines.slice(endLine + 1);
2832
- const result = [...before, ...formattedLines, ...after].join("\n");
2833
3154
  return result;
2834
3155
  }
3156
+ async function formatRange(text, _range, options) {
3157
+ return formatDocument(text, options);
3158
+ }
2835
3159
  function normalizePassageHeader(line) {
2836
3160
  const headerMatch = line.match(/^::\s+(.*)/);
2837
3161
  if (!headerMatch) return line;
@@ -2851,13 +3175,18 @@ function normalizePassageHeader(line) {
2851
3175
  }
2852
3176
  return `:: ${name}${suffix}`;
2853
3177
  }
2854
- var BLOCK_OPEN_REGEX, BLOCK_CLOSE_REGEX, PASSAGE_HEADER_REGEX, formatPlugin;
3178
+ var PASSAGE_HEADER_REGEX2, MACRO_OPEN_REGEX, MACRO_CLOSE_REGEX, DEFAULT_DEDENTING, formatPlugin;
2855
3179
  var init_format = __esm({
2856
3180
  "src/plugins/format.ts"() {
2857
3181
  "use strict";
2858
- BLOCK_OPEN_REGEX = /^\s*\{(if|for|switch|unless|widget)\b/i;
2859
- BLOCK_CLOSE_REGEX = /^\s*\{\/(if|for|switch|unless|widget)\b/i;
2860
- PASSAGE_HEADER_REGEX = /^(::)\s+/;
3182
+ init_macro_supplements();
3183
+ init_segment();
3184
+ init_prettier_bridge();
3185
+ init_placeholders();
3186
+ PASSAGE_HEADER_REGEX2 = /^(::)\s+/;
3187
+ MACRO_OPEN_REGEX = /^\{(?:[#.][a-zA-Z][\w-]*\s*)*([A-Za-z][\w-]*)\b/;
3188
+ MACRO_CLOSE_REGEX = /^\{\/([A-Za-z][\w-]*)\b/;
3189
+ DEFAULT_DEDENTING = /* @__PURE__ */ new Set(["else", "elseif", "next", "case", "default"]);
2861
3190
  formatPlugin = {
2862
3191
  id: "format",
2863
3192
  capabilities: {
@@ -2865,10 +3194,14 @@ var init_format = __esm({
2865
3194
  documentRangeFormattingProvider: true
2866
3195
  },
2867
3196
  initialize(ctx) {
2868
- ctx.connection.onDocumentFormatting((params) => {
3197
+ const formatOpts = {
3198
+ isBlock: (name) => ctx.workspace.macros.isBlock(name),
3199
+ isDedentingSubMacro: (name) => DEFAULT_DEDENTING.has(name.toLowerCase())
3200
+ };
3201
+ ctx.connection.onDocumentFormatting(async (params) => {
2869
3202
  const text = ctx.workspace.documents.getText(params.textDocument.uri);
2870
3203
  if (text === void 0) return [];
2871
- const formatted = formatDocument(text);
3204
+ const formatted = await formatDocument(text, formatOpts);
2872
3205
  if (formatted === text) return [];
2873
3206
  const lines = text.split("\n");
2874
3207
  return [{
@@ -2879,14 +3212,14 @@ var init_format = __esm({
2879
3212
  newText: formatted
2880
3213
  }];
2881
3214
  });
2882
- ctx.connection.onDocumentRangeFormatting((params) => {
3215
+ ctx.connection.onDocumentRangeFormatting(async (params) => {
2883
3216
  const text = ctx.workspace.documents.getText(params.textDocument.uri);
2884
3217
  if (text === void 0) return [];
2885
3218
  const range = {
2886
3219
  start: { line: params.range.start.line, character: params.range.start.character },
2887
3220
  end: { line: params.range.end.line, character: params.range.end.character }
2888
3221
  };
2889
- const formatted = formatRange(text, range);
3222
+ const formatted = await formatRange(text, range, formatOpts);
2890
3223
  if (formatted === text) return [];
2891
3224
  const lines = text.split("\n");
2892
3225
  return [{
@@ -2947,7 +3280,7 @@ async function runFormat(args2) {
2947
3280
  for (const filePath of uniqueFiles) {
2948
3281
  try {
2949
3282
  const text = readFileSync5(filePath, "utf-8");
2950
- const formatted = formatDocument(text);
3283
+ const formatted = await formatDocument(text);
2951
3284
  if (formatted !== text) {
2952
3285
  if (options.check) {
2953
3286
  const relative = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
@@ -2979,6 +3312,197 @@ var init_format2 = __esm({
2979
3312
  }
2980
3313
  });
2981
3314
 
3315
+ // src/mcp/server.ts
3316
+ var server_exports = {};
3317
+ __export(server_exports, {
3318
+ startMcpServer: () => startMcpServer
3319
+ });
3320
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
3321
+ import { resolve as resolve2 } from "node:path";
3322
+ import { pathToFileURL as pathToFileURL3, fileURLToPath as fileURLToPath3 } from "node:url";
3323
+ import { glob as glob3 } from "glob";
3324
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3325
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3326
+ import { z } from "zod";
3327
+ function filterBySeverity2(diagnostics, minSeverity) {
3328
+ const minIdx = SEVERITY_ORDER2.indexOf(minSeverity);
3329
+ return diagnostics.filter((d) => {
3330
+ const idx = SEVERITY_ORDER2.indexOf(d.severity);
3331
+ return idx >= minIdx;
3332
+ });
3333
+ }
3334
+ async function resolveFiles(pattern, cwd) {
3335
+ const matches = await glob3(pattern, {
3336
+ cwd,
3337
+ absolute: true,
3338
+ nodir: true
3339
+ });
3340
+ return [...new Set(matches)];
3341
+ }
3342
+ function findConfigRoot(files) {
3343
+ const dirs = files.map((f) => resolve2(f, ".."));
3344
+ let configRoot = dirs.reduce((a, b) => {
3345
+ while (!b.startsWith(a)) a = resolve2(a, "..");
3346
+ return a;
3347
+ });
3348
+ let search = configRoot;
3349
+ for (let i = 0; i < 10; i++) {
3350
+ if (findConfigFile(search)) {
3351
+ configRoot = search;
3352
+ break;
3353
+ }
3354
+ const parent = resolve2(search, "..");
3355
+ if (parent === search) break;
3356
+ search = parent;
3357
+ }
3358
+ return configRoot;
3359
+ }
3360
+ function createWorkspace(files) {
3361
+ const configRoot = findConfigRoot(files);
3362
+ const projectConfig = loadConfigFromDisk(configRoot);
3363
+ const workspace = new WorkspaceModel();
3364
+ const fileContents = /* @__PURE__ */ new Map();
3365
+ for (const filePath of files) {
3366
+ try {
3367
+ const text = readFileSync6(filePath, "utf-8");
3368
+ const uri = pathToFileURL3(filePath).toString();
3369
+ fileContents.set(uri, text);
3370
+ } catch {
3371
+ }
3372
+ }
3373
+ workspace.initialize(fileContents);
3374
+ if (Object.keys(projectConfig.macros).length > 0) {
3375
+ workspace.macros.loadSupplements(projectConfig.macros);
3376
+ }
3377
+ return workspace;
3378
+ }
3379
+ async function startMcpServer() {
3380
+ const server = new McpServer({
3381
+ name: "spindle-lsp",
3382
+ version: "0.3.5"
3383
+ });
3384
+ server.tool(
3385
+ "spindle_check",
3386
+ "Run diagnostics on .tw/.twee files. Returns structured diagnostic results.",
3387
+ {
3388
+ path: z.string().default("**/*.{tw,twee}").describe("Glob pattern or directory to check"),
3389
+ severity: z.enum(["error", "warning", "info", "hint"]).optional().describe("Minimum severity to include")
3390
+ },
3391
+ async (args2) => {
3392
+ const cwd = process.cwd();
3393
+ const files = await resolveFiles(args2.path, cwd);
3394
+ if (files.length === 0) {
3395
+ return {
3396
+ content: [{ type: "text", text: JSON.stringify([], null, 2) }]
3397
+ };
3398
+ }
3399
+ const workspace = createWorkspace(files);
3400
+ try {
3401
+ const results = [];
3402
+ for (const [uri] of workspace.documents.getUris().map((u) => [u])) {
3403
+ let diags = computeDiagnostics(uri, workspace);
3404
+ if (args2.severity) {
3405
+ diags = filterBySeverity2(diags, args2.severity);
3406
+ }
3407
+ const filePath = fileURLToPath3(uri);
3408
+ const relativePath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
3409
+ for (const d of diags) {
3410
+ results.push({
3411
+ file: relativePath,
3412
+ line: d.range.start.line + 1,
3413
+ column: d.range.start.character + 1,
3414
+ severity: d.severity,
3415
+ code: d.code,
3416
+ message: d.message
3417
+ });
3418
+ }
3419
+ }
3420
+ return {
3421
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
3422
+ };
3423
+ } finally {
3424
+ workspace.dispose();
3425
+ }
3426
+ }
3427
+ );
3428
+ server.tool(
3429
+ "spindle_format",
3430
+ "Format .tw/.twee files in place.",
3431
+ {
3432
+ path: z.string().default("**/*.{tw,twee}").describe("Glob pattern or directory to format")
3433
+ },
3434
+ async (args2) => {
3435
+ const cwd = process.cwd();
3436
+ const files = await resolveFiles(args2.path, cwd);
3437
+ let formatted = 0;
3438
+ let unchanged = 0;
3439
+ const changedFiles = [];
3440
+ for (const filePath of files) {
3441
+ try {
3442
+ const text = readFileSync6(filePath, "utf-8");
3443
+ const result = await formatDocument(text);
3444
+ if (result !== text) {
3445
+ writeFileSync2(filePath, result, "utf-8");
3446
+ formatted++;
3447
+ const relativePath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
3448
+ changedFiles.push(relativePath);
3449
+ } else {
3450
+ unchanged++;
3451
+ }
3452
+ } catch {
3453
+ }
3454
+ }
3455
+ const output = { formatted, unchanged, files: changedFiles };
3456
+ return {
3457
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
3458
+ };
3459
+ }
3460
+ );
3461
+ server.tool(
3462
+ "spindle_format_check",
3463
+ "Check formatting without modifying files. Returns list of files that need formatting.",
3464
+ {
3465
+ path: z.string().default("**/*.{tw,twee}").describe("Glob pattern or directory to check")
3466
+ },
3467
+ async (args2) => {
3468
+ const cwd = process.cwd();
3469
+ const files = await resolveFiles(args2.path, cwd);
3470
+ const needsFormatting = [];
3471
+ const alreadyFormatted = [];
3472
+ for (const filePath of files) {
3473
+ try {
3474
+ const text = readFileSync6(filePath, "utf-8");
3475
+ const result = await formatDocument(text);
3476
+ const relativePath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
3477
+ if (result !== text) {
3478
+ needsFormatting.push(relativePath);
3479
+ } else {
3480
+ alreadyFormatted.push(relativePath);
3481
+ }
3482
+ } catch {
3483
+ }
3484
+ }
3485
+ const output = { needsFormatting, alreadyFormatted };
3486
+ return {
3487
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
3488
+ };
3489
+ }
3490
+ );
3491
+ const transport = new StdioServerTransport();
3492
+ await server.connect(transport);
3493
+ }
3494
+ var SEVERITY_ORDER2;
3495
+ var init_server = __esm({
3496
+ "src/mcp/server.ts"() {
3497
+ "use strict";
3498
+ init_workspace_model();
3499
+ init_config_loader();
3500
+ init_diagnostics();
3501
+ init_format();
3502
+ SEVERITY_ORDER2 = ["hint", "info", "warning", "error"];
3503
+ }
3504
+ });
3505
+
2982
3506
  // src/core/plugin/plugin-loader.ts
2983
3507
  function loadPlugins(allPlugins2, config) {
2984
3508
  const disabled = new Set(config.disabledPlugins ?? []);
@@ -4833,8 +5357,8 @@ var init_plugins = __esm({
4833
5357
  });
4834
5358
 
4835
5359
  // src/server/server.ts
4836
- var server_exports = {};
4837
- __export(server_exports, {
5360
+ var server_exports2 = {};
5361
+ __export(server_exports2, {
4838
5362
  startServer: () => startServer
4839
5363
  });
4840
5364
  import {
@@ -4844,18 +5368,18 @@ import {
4844
5368
  FileChangeType
4845
5369
  } from "vscode-languageserver/node.js";
4846
5370
  import { TextDocument } from "vscode-languageserver-textdocument";
4847
- import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL3 } from "node:url";
4848
- import { readFileSync as readFileSync6 } from "node:fs";
4849
- import { glob as glob3 } from "glob";
5371
+ import { fileURLToPath as fileURLToPath4, pathToFileURL as pathToFileURL4 } from "node:url";
5372
+ import { readFileSync as readFileSync7 } from "node:fs";
5373
+ import { glob as glob4 } from "glob";
4850
5374
  function uriToFsPath(uri) {
4851
5375
  try {
4852
- return fileURLToPath3(uri);
5376
+ return fileURLToPath4(uri);
4853
5377
  } catch {
4854
5378
  return uri;
4855
5379
  }
4856
5380
  }
4857
5381
  function fsPathToUri(fsPath) {
4858
- return pathToFileURL3(fsPath).toString();
5382
+ return pathToFileURL4(fsPath).toString();
4859
5383
  }
4860
5384
  function startServer(_args) {
4861
5385
  const connection = createConnection(ProposedFeatures.all);
@@ -4954,7 +5478,7 @@ function startServer(_args) {
4954
5478
  } else {
4955
5479
  try {
4956
5480
  const fsPath = uriToFsPath(change.uri);
4957
- const text = readFileSync6(fsPath, "utf-8");
5481
+ const text = readFileSync7(fsPath, "utf-8");
4958
5482
  if (workspace.documents.has(change.uri)) {
4959
5483
  workspace.documents.update(change.uri, text);
4960
5484
  } else {
@@ -4977,14 +5501,14 @@ async function scanWorkspaceFiles(root) {
4977
5501
  const contents = /* @__PURE__ */ new Map();
4978
5502
  if (!root) return contents;
4979
5503
  try {
4980
- const files = await glob3("**/*.{tw,twee}", {
5504
+ const files = await glob4("**/*.{tw,twee}", {
4981
5505
  cwd: root,
4982
5506
  absolute: true,
4983
5507
  nodir: true
4984
5508
  });
4985
5509
  for (const filePath of files) {
4986
5510
  try {
4987
- const text = readFileSync6(filePath, "utf-8");
5511
+ const text = readFileSync7(filePath, "utf-8");
4988
5512
  const uri = fsPathToUri(filePath);
4989
5513
  contents.set(uri, text);
4990
5514
  } catch {
@@ -4994,7 +5518,7 @@ async function scanWorkspaceFiles(root) {
4994
5518
  }
4995
5519
  return contents;
4996
5520
  }
4997
- var init_server = __esm({
5521
+ var init_server2 = __esm({
4998
5522
  "src/server/server.ts"() {
4999
5523
  "use strict";
5000
5524
  init_workspace_model();
@@ -5015,8 +5539,11 @@ if (args[0] === "check") {
5015
5539
  const { runFormat: runFormat2 } = await Promise.resolve().then(() => (init_format2(), format_exports));
5016
5540
  const exitCode = await runFormat2(args.slice(1));
5017
5541
  process.exit(exitCode);
5542
+ } else if (args[0] === "mcp") {
5543
+ const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
5544
+ await startMcpServer2();
5018
5545
  } else {
5019
- const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
5546
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server2(), server_exports2));
5020
5547
  startServer2(args);
5021
5548
  }
5022
5549
  //# sourceMappingURL=bin.js.map