@blokjs/lsp-server 0.2.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/CHANGELOG.md +15 -0
- package/dist/completion.d.ts +11 -0
- package/dist/completion.js +269 -0
- package/dist/completion.js.map +1 -0
- package/dist/constants.d.ts +37 -0
- package/dist/constants.js +161 -0
- package/dist/constants.js.map +1 -0
- package/dist/diagnostics.d.ts +17 -0
- package/dist/diagnostics.js +466 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/hover.d.ts +12 -0
- package/dist/hover.js +118 -0
- package/dist/hover.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +130 -0
- package/dist/server.js.map +1 -0
- package/editors/emacs-lsp.el +21 -0
- package/editors/helix-languages.toml +12 -0
- package/editors/neovim.lua +59 -0
- package/editors/sublime-lsp.json +16 -0
- package/package.json +40 -0
- package/src/__tests__/completion.test.ts +184 -0
- package/src/__tests__/constants.test.ts +142 -0
- package/src/__tests__/diagnostics.test.ts +513 -0
- package/src/__tests__/hover.test.ts +227 -0
- package/src/completion.ts +308 -0
- package/src/constants.ts +194 -0
- package/src/diagnostics.ts +493 -0
- package/src/hover.ts +135 -0
- package/src/server.ts +162 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +9 -0
package/src/hover.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { type Hover, MarkupKind, Position, Range } from "vscode-languageserver";
|
|
2
|
+
import { FIELD_DOCS, type HoverDoc, STEP_FIELD_DOCS, TRIGGER_DOCS } from "./constants";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Provides hover documentation for Blok workflow JSON files via LSP.
|
|
6
|
+
*
|
|
7
|
+
* Shows contextual documentation when hovering over:
|
|
8
|
+
* - Trigger type keys (http, grpc, cron, queue, etc.)
|
|
9
|
+
* - Workflow fields (name, version, steps, nodes, etc.)
|
|
10
|
+
* - Node configuration fields (inputs, conditions, set_var)
|
|
11
|
+
* - Step fields (node, type, runtime)
|
|
12
|
+
* - HTTP method values, runtime type values, node packages
|
|
13
|
+
*/
|
|
14
|
+
export function getHover(text: string, line: number, character: number): Hover | null {
|
|
15
|
+
const lines = text.split("\n");
|
|
16
|
+
if (line >= lines.length) return null;
|
|
17
|
+
|
|
18
|
+
const lineText = lines[line];
|
|
19
|
+
|
|
20
|
+
// Find the quoted word at the cursor position
|
|
21
|
+
const wordInfo = findQuotedWordAt(lineText, character);
|
|
22
|
+
if (!wordInfo) return null;
|
|
23
|
+
|
|
24
|
+
const { word, startChar, endChar } = wordInfo;
|
|
25
|
+
const range = Range.create(Position.create(line, startChar), Position.create(line, endChar));
|
|
26
|
+
|
|
27
|
+
// Check if it's a key (followed by colon)
|
|
28
|
+
const afterWord = lineText.substring(endChar).trimStart();
|
|
29
|
+
const isKey = afterWord.startsWith(":");
|
|
30
|
+
|
|
31
|
+
if (isKey) {
|
|
32
|
+
// Trigger type documentation
|
|
33
|
+
if (TRIGGER_DOCS[word]) {
|
|
34
|
+
return createDocHover(TRIGGER_DOCS[word], range);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Field documentation
|
|
38
|
+
if (FIELD_DOCS[word]) {
|
|
39
|
+
return createDocHover(FIELD_DOCS[word], range);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Step field documentation
|
|
43
|
+
if (STEP_FIELD_DOCS[word]) {
|
|
44
|
+
return createDocHover(STEP_FIELD_DOCS[word], range);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Value-based hover
|
|
49
|
+
if (!isKey) {
|
|
50
|
+
// HTTP methods
|
|
51
|
+
if (["GET", "POST", "PUT", "DELETE", "PATCH", "ANY"].includes(word)) {
|
|
52
|
+
return {
|
|
53
|
+
contents: {
|
|
54
|
+
kind: MarkupKind.Markdown,
|
|
55
|
+
value: `**HTTP Method: ${word}**\n\nHTTP request method that will trigger this workflow.`,
|
|
56
|
+
},
|
|
57
|
+
range,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Runtime type values (runtime.go, runtime.python3, etc.)
|
|
62
|
+
if (word.startsWith("runtime.")) {
|
|
63
|
+
const lang = word.replace("runtime.", "");
|
|
64
|
+
return {
|
|
65
|
+
contents: {
|
|
66
|
+
kind: MarkupKind.Markdown,
|
|
67
|
+
value: `**Runtime Type: ${lang}**\n\nExecutes this node using the ${lang} runtime adapter. The node code must be written in ${lang} and served via the Blok runtime protocol (HTTP/gRPC).`,
|
|
68
|
+
},
|
|
69
|
+
range,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Common node packages
|
|
74
|
+
if (word === "@blokjs/api-call") {
|
|
75
|
+
return {
|
|
76
|
+
contents: {
|
|
77
|
+
kind: MarkupKind.Markdown,
|
|
78
|
+
value:
|
|
79
|
+
"**@blokjs/api-call**\n\nMakes HTTP API calls to external services.\n\n**Inputs:** `url`, `method`, `headers`, `body`, `responseType`",
|
|
80
|
+
},
|
|
81
|
+
range,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (word === "@blokjs/if-else") {
|
|
85
|
+
return {
|
|
86
|
+
contents: {
|
|
87
|
+
kind: MarkupKind.Markdown,
|
|
88
|
+
value:
|
|
89
|
+
"**@blokjs/if-else**\n\nConditional branching node. Evaluates JavaScript conditions against the workflow context.\n\nConfigure conditions in the `nodes` section using the `conditions` array.",
|
|
90
|
+
},
|
|
91
|
+
range,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function findQuotedWordAt(
|
|
100
|
+
line: string,
|
|
101
|
+
character: number,
|
|
102
|
+
): { word: string; startChar: number; endChar: number } | null {
|
|
103
|
+
// Find all quoted strings in the line and check if cursor is inside one
|
|
104
|
+
const regex = /"([^"]*)"/g;
|
|
105
|
+
|
|
106
|
+
for (const match of line.matchAll(regex)) {
|
|
107
|
+
const start = match.index; // position of opening quote
|
|
108
|
+
const end = match.index + match[0].length; // position after closing quote
|
|
109
|
+
|
|
110
|
+
if (character >= start && character <= end) {
|
|
111
|
+
return {
|
|
112
|
+
word: match[1],
|
|
113
|
+
startChar: start,
|
|
114
|
+
endChar: end,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function createDocHover(doc: HoverDoc, range: Range): Hover {
|
|
123
|
+
let value = `**${doc.title}**\n\n${doc.description}\n\n`;
|
|
124
|
+
if (doc.example) {
|
|
125
|
+
value += "```json\n" + doc.example + "\n```";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
contents: {
|
|
130
|
+
kind: MarkupKind.Markdown,
|
|
131
|
+
value,
|
|
132
|
+
},
|
|
133
|
+
range,
|
|
134
|
+
};
|
|
135
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
|
+
import {
|
|
4
|
+
type CompletionParams,
|
|
5
|
+
DidChangeConfigurationNotification,
|
|
6
|
+
type HoverParams,
|
|
7
|
+
type InitializeParams,
|
|
8
|
+
type InitializeResult,
|
|
9
|
+
ProposedFeatures,
|
|
10
|
+
TextDocumentSyncKind,
|
|
11
|
+
TextDocuments,
|
|
12
|
+
createConnection,
|
|
13
|
+
} from "vscode-languageserver/node";
|
|
14
|
+
import { getCompletions } from "./completion";
|
|
15
|
+
import { validateWorkflow } from "./diagnostics";
|
|
16
|
+
import { getHover } from "./hover";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Blok Workflow Language Server
|
|
20
|
+
*
|
|
21
|
+
* Provides workflow intelligence for any LSP-compatible editor:
|
|
22
|
+
* - Diagnostics: Real-time validation of workflow JSON files
|
|
23
|
+
* - Completion: Contextual auto-completion for triggers, steps, runtimes, etc.
|
|
24
|
+
* - Hover: Rich documentation on hover for workflow fields and values
|
|
25
|
+
*
|
|
26
|
+
* Communication: stdio (default) or TCP
|
|
27
|
+
* File types: JSON files matching `**/workflows/**/*.json` or `blok.workflow.json`
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Create connection and document manager
|
|
31
|
+
const connection = createConnection(ProposedFeatures.all);
|
|
32
|
+
const documents = new TextDocuments(TextDocument);
|
|
33
|
+
|
|
34
|
+
let hasConfigurationCapability = false;
|
|
35
|
+
let hasWorkspaceFolderCapability = false;
|
|
36
|
+
|
|
37
|
+
// Server settings
|
|
38
|
+
interface BlokLspSettings {
|
|
39
|
+
workflowGlob: string;
|
|
40
|
+
maxDiagnostics: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const defaultSettings: BlokLspSettings = {
|
|
44
|
+
workflowGlob: "**/workflows/**/*.json",
|
|
45
|
+
maxDiagnostics: 100,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let globalSettings: BlokLspSettings = defaultSettings;
|
|
49
|
+
const documentSettings = new Map<string, BlokLspSettings>();
|
|
50
|
+
|
|
51
|
+
connection.onInitialize((params: InitializeParams): InitializeResult => {
|
|
52
|
+
const capabilities = params.capabilities;
|
|
53
|
+
|
|
54
|
+
hasConfigurationCapability = !!(capabilities.workspace && capabilities.workspace.configuration);
|
|
55
|
+
hasWorkspaceFolderCapability = !!(capabilities.workspace && capabilities.workspace.workspaceFolders);
|
|
56
|
+
|
|
57
|
+
const result: InitializeResult = {
|
|
58
|
+
capabilities: {
|
|
59
|
+
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
60
|
+
completionProvider: {
|
|
61
|
+
resolveProvider: false,
|
|
62
|
+
triggerCharacters: ['"', ":"],
|
|
63
|
+
},
|
|
64
|
+
hoverProvider: true,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (hasWorkspaceFolderCapability) {
|
|
69
|
+
result.capabilities.workspace = {
|
|
70
|
+
workspaceFolders: {
|
|
71
|
+
supported: true,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
connection.onInitialized(() => {
|
|
80
|
+
if (hasConfigurationCapability) {
|
|
81
|
+
connection.client.register(DidChangeConfigurationNotification.type, undefined);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Configuration handling
|
|
86
|
+
connection.onDidChangeConfiguration((change) => {
|
|
87
|
+
if (hasConfigurationCapability) {
|
|
88
|
+
documentSettings.clear();
|
|
89
|
+
} else {
|
|
90
|
+
globalSettings = change.settings?.blok || defaultSettings;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Revalidate all open documents
|
|
94
|
+
for (const doc of documents.all()) {
|
|
95
|
+
validateDocument(doc);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function getDocumentSettings(resource: string): BlokLspSettings {
|
|
100
|
+
if (!hasConfigurationCapability) {
|
|
101
|
+
return globalSettings;
|
|
102
|
+
}
|
|
103
|
+
let result = documentSettings.get(resource);
|
|
104
|
+
if (!result) {
|
|
105
|
+
result = globalSettings;
|
|
106
|
+
documentSettings.set(resource, result);
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Document validation
|
|
112
|
+
function isWorkflowFile(uri: string): boolean {
|
|
113
|
+
// Match workflow files by path pattern
|
|
114
|
+
return /workflows?[/\\].*\.json$/i.test(uri) || /\.workflow\.json$/i.test(uri) || /blok\.json$/i.test(uri);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateDocument(document: TextDocument): void {
|
|
118
|
+
if (!isWorkflowFile(document.uri)) return;
|
|
119
|
+
|
|
120
|
+
const text = document.getText();
|
|
121
|
+
const diagnostics = validateWorkflow(text);
|
|
122
|
+
const settings = getDocumentSettings(document.uri);
|
|
123
|
+
|
|
124
|
+
// Limit diagnostics if configured
|
|
125
|
+
const limited = diagnostics.slice(0, settings.maxDiagnostics);
|
|
126
|
+
connection.sendDiagnostics({ uri: document.uri, diagnostics: limited });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Validate on open and change
|
|
130
|
+
documents.onDidChangeContent((change) => {
|
|
131
|
+
validateDocument(change.document);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
documents.onDidClose((event) => {
|
|
135
|
+
documentSettings.delete(event.document.uri);
|
|
136
|
+
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Completion
|
|
140
|
+
connection.onCompletion((params: CompletionParams) => {
|
|
141
|
+
const document = documents.get(params.textDocument.uri);
|
|
142
|
+
if (!document) return [];
|
|
143
|
+
if (!isWorkflowFile(document.uri)) return [];
|
|
144
|
+
|
|
145
|
+
const text = document.getText();
|
|
146
|
+
const offset = document.offsetAt(params.position);
|
|
147
|
+
return getCompletions(text, offset);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Hover
|
|
151
|
+
connection.onHover((params: HoverParams) => {
|
|
152
|
+
const document = documents.get(params.textDocument.uri);
|
|
153
|
+
if (!document) return null;
|
|
154
|
+
if (!isWorkflowFile(document.uri)) return null;
|
|
155
|
+
|
|
156
|
+
const text = document.getText();
|
|
157
|
+
return getHover(text, params.position.line, params.position.character);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Start listening
|
|
161
|
+
documents.listen(connection);
|
|
162
|
+
connection.listen();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"lib": ["ES2022"],
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"moduleResolution": "node"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "**/__tests__/**"]
|
|
18
|
+
}
|