11import * as path from "node:path" ;
2- import type { ExtensionContext } from "vscode" ;
2+ import { parse } from "@graplix/language" ;
3+ import * as vscode from "vscode" ;
34import {
45 LanguageClient ,
56 type LanguageClientOptions ,
@@ -8,11 +9,271 @@ import {
89} from "vscode-languageclient/node" ;
910
1011let client : LanguageClient ;
12+ const validationTimers = new Map < string , ReturnType < typeof setTimeout > > ( ) ;
13+ let outputChannel : vscode . OutputChannel | undefined ;
14+
15+ interface GraplixTemplate {
16+ readonly startOffset : number ;
17+ readonly endOffset : number ;
18+ readonly content : string ;
19+ readonly hasInterpolation : boolean ;
20+ }
21+
22+ function extractGraplixTemplates ( text : string ) : readonly GraplixTemplate [ ] {
23+ const templates : GraplixTemplate [ ] = [ ] ;
24+ const tag = "graplix" ;
25+
26+ const isIdentifierCharacter = ( char : string | undefined ) : boolean => {
27+ return char !== undefined && / [ A - Z a - z 0 - 9 _ $ ] / . test ( char ) ;
28+ } ;
29+
30+ const skipWhitespaceAndComments = ( startIndex : number ) : number => {
31+ let index = startIndex ;
32+
33+ while ( index < text . length ) {
34+ const char = text [ index ] ;
35+ const next = text [ index + 1 ] ;
36+
37+ if ( char === " " || char === "\t" || char === "\r" || char === "\n" ) {
38+ index += 1 ;
39+ continue ;
40+ }
41+
42+ if ( char === "/" && next === "/" ) {
43+ index += 2 ;
44+ while ( index < text . length && text [ index ] !== "\n" ) {
45+ index += 1 ;
46+ }
47+ continue ;
48+ }
49+
50+ if ( char === "/" && next === "*" ) {
51+ index += 2 ;
52+ while ( index + 1 < text . length ) {
53+ if ( text [ index ] === "*" && text [ index + 1 ] === "/" ) {
54+ index += 2 ;
55+ break ;
56+ }
57+ index += 1 ;
58+ }
59+ continue ;
60+ }
61+
62+ break ;
63+ }
64+
65+ return index ;
66+ } ;
67+
68+ let searchIndex = 0 ;
69+
70+ while ( searchIndex < text . length ) {
71+ const matchIndex = text . indexOf ( tag , searchIndex ) ;
72+ if ( matchIndex < 0 ) {
73+ break ;
74+ }
75+
76+ const previous = text [ matchIndex - 1 ] ;
77+ const nextAfterTag = text [ matchIndex + tag . length ] ;
78+ if (
79+ isIdentifierCharacter ( previous ) ||
80+ isIdentifierCharacter ( nextAfterTag )
81+ ) {
82+ searchIndex = matchIndex + tag . length ;
83+ continue ;
84+ }
85+
86+ const templateDelimiterIndex = skipWhitespaceAndComments (
87+ matchIndex + tag . length ,
88+ ) ;
89+ if ( text [ templateDelimiterIndex ] !== "`" ) {
90+ searchIndex = matchIndex + tag . length ;
91+ continue ;
92+ }
93+
94+ const templateStart = templateDelimiterIndex + 1 ;
95+ let cursor = templateStart ;
96+ let hasInterpolation = false ;
97+
98+ while ( cursor < text . length ) {
99+ const char = text [ cursor ] ;
100+
101+ if ( char === "\\" ) {
102+ cursor += 2 ;
103+ continue ;
104+ }
105+
106+ if ( char === "$" && text [ cursor + 1 ] === "{" ) {
107+ hasInterpolation = true ;
108+ cursor += 2 ;
109+ let interpolationDepth = 1 ;
110+
111+ while ( cursor < text . length && interpolationDepth > 0 ) {
112+ const interpolationChar = text [ cursor ] ;
113+ if ( interpolationChar === "\\" ) {
114+ cursor += 2 ;
115+ continue ;
116+ }
117+ if ( interpolationChar === "{" ) {
118+ interpolationDepth += 1 ;
119+ } else if ( interpolationChar === "}" ) {
120+ interpolationDepth -= 1 ;
121+ }
122+ cursor += 1 ;
123+ }
124+
125+ continue ;
126+ }
127+
128+ if ( char === "`" ) {
129+ templates . push ( {
130+ startOffset : templateStart ,
131+ endOffset : cursor ,
132+ content : text . slice ( templateStart , cursor ) ,
133+ hasInterpolation,
134+ } ) ;
135+ break ;
136+ }
137+
138+ cursor += 1 ;
139+ }
140+
141+ searchIndex = cursor + 1 ;
142+ }
143+
144+ return templates ;
145+ }
146+
147+ function severityToVscode (
148+ severity : number | undefined ,
149+ ) : vscode . DiagnosticSeverity {
150+ if ( severity === 1 ) {
151+ return vscode . DiagnosticSeverity . Error ;
152+ }
153+ if ( severity === 2 ) {
154+ return vscode . DiagnosticSeverity . Warning ;
155+ }
156+ if ( severity === 3 ) {
157+ return vscode . DiagnosticSeverity . Information ;
158+ }
159+ if ( severity === 4 ) {
160+ return vscode . DiagnosticSeverity . Hint ;
161+ }
162+
163+ return vscode . DiagnosticSeverity . Error ;
164+ }
165+
166+ async function validateGraplixTemplates (
167+ document : vscode . TextDocument ,
168+ collection : vscode . DiagnosticCollection ,
169+ ) : Promise < void > {
170+ if (
171+ document . languageId !== "typescript" &&
172+ document . languageId !== "typescriptreact"
173+ ) {
174+ collection . delete ( document . uri ) ;
175+ return ;
176+ }
177+
178+ const version = document . version ;
179+ const templates = extractGraplixTemplates ( document . getText ( ) ) ;
180+ const diagnostics : vscode . Diagnostic [ ] = [ ] ;
181+
182+ for ( const [ index , template ] of templates . entries ( ) ) {
183+ const startPosition = document . positionAt ( template . startOffset ) ;
184+
185+ if ( template . hasInterpolation ) {
186+ const interpolationDiagnostic = new vscode . Diagnostic (
187+ new vscode . Range (
188+ startPosition ,
189+ document . positionAt ( template . endOffset ) ,
190+ ) ,
191+ "graplix tagged template with interpolation is not validated." ,
192+ vscode . DiagnosticSeverity . Warning ,
193+ ) ;
194+ interpolationDiagnostic . source = "graplix" ;
195+ diagnostics . push ( interpolationDiagnostic ) ;
196+ continue ;
197+ }
198+
199+ const ext = "graplix" ;
200+ const filename = `template-${ encodeURIComponent ( document . uri . toString ( ) ) } -${ index } .${ ext } ` ;
201+
202+ const graplixDocument = await parse ( template . content , {
203+ documentUri : `memory://graplix/${ filename } ` ,
204+ validation : true ,
205+ } ) ;
206+
207+ for ( const issue of graplixDocument . diagnostics ?? [ ] ) {
208+ const issueRange = issue . range ;
209+ const range =
210+ issueRange === undefined
211+ ? new vscode . Range ( startPosition , startPosition )
212+ : new vscode . Range (
213+ new vscode . Position (
214+ startPosition . line + issueRange . start . line ,
215+ issueRange . start . line === 0
216+ ? startPosition . character + issueRange . start . character
217+ : issueRange . start . character ,
218+ ) ,
219+ new vscode . Position (
220+ startPosition . line + issueRange . end . line ,
221+ issueRange . end . line === 0
222+ ? startPosition . character + issueRange . end . character
223+ : issueRange . end . character ,
224+ ) ,
225+ ) ;
226+
227+ const diagnostic = new vscode . Diagnostic (
228+ range ,
229+ issue . message ,
230+ severityToVscode ( issue . severity ) ,
231+ ) ;
232+ diagnostic . source = "graplix" ;
233+ diagnostics . push ( diagnostic ) ;
234+ }
235+ }
236+
237+ if ( version !== document . version ) {
238+ return ;
239+ }
240+
241+ collection . set ( document . uri , diagnostics ) ;
242+ }
243+
244+ function scheduleTemplateValidation (
245+ document : vscode . TextDocument ,
246+ collection : vscode . DiagnosticCollection ,
247+ ) : void {
248+ const key = document . uri . toString ( ) ;
249+ const pending = validationTimers . get ( key ) ;
250+ if ( pending !== undefined ) {
251+ clearTimeout ( pending ) ;
252+ }
253+
254+ validationTimers . set (
255+ key ,
256+ setTimeout ( async ( ) => {
257+ validationTimers . delete ( key ) ;
258+ try {
259+ await validateGraplixTemplates ( document , collection ) ;
260+ } catch ( error ) {
261+ const message = error instanceof Error ? error . message : String ( error ) ;
262+ outputChannel ?. appendLine (
263+ `[graplix-template] validation failed for ${ document . uri . toString ( ) } : ${ message } ` ,
264+ ) ;
265+ }
266+ } , 120 ) ,
267+ ) ;
268+ }
11269
12270/**
13271 * Activates the Graplix VS Code extension and starts the language client.
14272 */
15- export function activate ( context : ExtensionContext ) {
273+ export function activate ( context : vscode . ExtensionContext ) {
274+ outputChannel = vscode . window . createOutputChannel ( "Graplix" ) ;
275+ context . subscriptions . push ( outputChannel ) ;
276+
16277 // The server is implemented in node
17278 const serverModule = context . asAbsolutePath (
18279 path . join ( "dist" , "language-server.cjs" ) ,
@@ -52,6 +313,40 @@ export function activate(context: ExtensionContext) {
52313
53314 // Start the client. This will also launch the server
54315 client . start ( ) ;
316+
317+ const diagnostics =
318+ vscode . languages . createDiagnosticCollection ( "graplix-template" ) ;
319+ context . subscriptions . push ( diagnostics ) ;
320+
321+ for ( const document of vscode . workspace . textDocuments ) {
322+ scheduleTemplateValidation ( document , diagnostics ) ;
323+ }
324+
325+ context . subscriptions . push (
326+ vscode . workspace . onDidOpenTextDocument ( ( document ) => {
327+ scheduleTemplateValidation ( document , diagnostics ) ;
328+ } ) ,
329+ vscode . workspace . onDidChangeTextDocument ( ( event ) => {
330+ scheduleTemplateValidation ( event . document , diagnostics ) ;
331+ } ) ,
332+ vscode . workspace . onDidCloseTextDocument ( ( document ) => {
333+ diagnostics . delete ( document . uri ) ;
334+ const key = document . uri . toString ( ) ;
335+ const pending = validationTimers . get ( key ) ;
336+ if ( pending !== undefined ) {
337+ clearTimeout ( pending ) ;
338+ validationTimers . delete ( key ) ;
339+ }
340+ } ) ,
341+ vscode . workspace . onDidSaveTextDocument ( ( document ) => {
342+ scheduleTemplateValidation ( document , diagnostics ) ;
343+ } ) ,
344+ vscode . window . onDidChangeActiveTextEditor ( ( editor ) => {
345+ if ( editor !== undefined ) {
346+ scheduleTemplateValidation ( editor . document , diagnostics ) ;
347+ }
348+ } ) ,
349+ ) ;
55350}
56351
57352/**
0 commit comments