• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / vscode-brightscript-language / 29039456111

09 Jul 2026 06:06PM UTC coverage: 59.231% (+1.1%) from 58.144%
29039456111

push

github

web-flow
Fix nested namespace display and nesting in outline (#848)

2493 of 4644 branches covered (53.68%)

Branch coverage included in aggregate %.

21 of 42 new or added lines in 1 file covered. (50.0%)

3946 of 6227 relevant lines covered (63.37%)

41.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.39
/src/DeclarationProvider.ts
1
import * as fs from 'fs-extra';
1✔
2
import * as iconv from 'iconv-lite';
1✔
3
import * as vscode from 'vscode';
1✔
4
import * as path from 'path';
1✔
5

6
import type {
7
    Event,
8
    Uri
9
} from 'vscode';
10
import {
1✔
11
    Disposable,
12
    EventEmitter, Location,
13
    Position,
14
    Range, SymbolInformation,
15
    SymbolKind
16
} from 'vscode';
17

18
import { BrightScriptDeclaration } from './BrightScriptDeclaration';
1✔
19
import { util } from './util';
1✔
20

21
///////////////////////////////////////////////////////////////////////////////////////////////////////////
22
// CREDIT WHERE CREDIT IS DUE
23
// georgejecook: I lifted most of the declaration and symbol work from sasami's era basic implementation
24
// at https://github.com/sasami/vscode-erabasic and hacked it in with some basic changes
25
///////////////////////////////////////////////////////////////////////////////////////////////////////////
26

27
export function* iterlines(input: string): IterableIterator<[number, string]> {
1✔
28
    const lines = input.split(/\r?\n/);
4✔
29
    for (let i = 0; i < lines.length; i++) {
4✔
30
        const text = lines[i];
16✔
31
        if (/^\s*(?:$|;(?![!#];))/.test(text)) {
16!
32
            continue;
×
33
        }
34
        yield [i, text];
16✔
35
    }
36
}
37

38
export class WorkspaceEncoding {
1✔
39

40
    constructor() {
41
        this.reset();
87✔
42
    }
43

44
    private encoding: string[][];
45

46
    public find(path: string): string {
47
        return this.encoding.find((v) => path.startsWith(v[0]))?.[1] ?? 'utf8';
2!
48
    }
49

50
    public reset() {
51
        this.encoding = [];
87✔
52
        for (const folder of vscode.workspace.workspaceFolders ?? []) {
87!
53
            this.encoding.push([folder.uri.fsPath, this.getConfiguration(folder.uri)]);
×
54
        }
55
    }
56

57
    private getConfiguration(uri: Uri): string {
58
        const encoding: string = util.getConfiguration('files', uri).get('encoding', 'utf8');
×
59
        if (encoding === 'utf8bom') {
×
60
            return 'utf8'; // iconv-lite removes bom by default when decoding, so this is fine
×
61
        }
62
        return encoding;
×
63
    }
64
}
65

66
export class DeclarationChangeEvent {
1✔
67
    constructor(public uri: Uri, public decls: BrightScriptDeclaration[]) {
1✔
68
    }
69
}
70

71
export class DeclarationDeleteEvent {
1✔
72
    constructor(public uri: Uri) {
1✔
73
    }
74
}
75

76
export class DeclarationProvider implements Disposable {
1✔
77
    constructor() {
78
        const subscriptions: Disposable[] = [];
82✔
79

80
        const watcher = vscode.workspace.createFileSystemWatcher('**/*.{brs,bs}');
82✔
81
        watcher.onDidCreate(this.onDidChangeFile, this);
82✔
82
        watcher.onDidChange(this.onDidChangeFile, this);
82✔
83
        watcher.onDidDelete(this.onDidDeleteFile, this);
82✔
84
        subscriptions.push(watcher);
82✔
85

86
        vscode.workspace.onDidChangeConfiguration(this.onDidChangeWorkspace, this, subscriptions);
82✔
87
        vscode.workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspace, this, subscriptions);
82✔
88

89
        this.disposable = Disposable.from(...subscriptions);
82✔
90
        void this.flush();
82✔
91
    }
92
    public cache = new Map<string, BrightScriptDeclaration[]>();
82✔
93
    private fullscan = true;
82✔
94

95
    private dirty = new Map<string, Uri>();
82✔
96
    private fileNamespaces = new Map<Uri, Set<string>>();
82✔
97
    private fileInterfaces = new Map<Uri, Set<string>>();
82✔
98
    private fileEnums = new Map<Uri, Set<string>>();
82✔
99
    private fileClasses = new Map<Uri, Set<string>>();
82✔
100
    private allNamespaces = new Map<string, BrightScriptDeclaration>();
82✔
101
    private allClasses = new Map<string, BrightScriptDeclaration>();
82✔
102
    private allEnums = new Map<string, BrightScriptDeclaration>();
82✔
103
    private allInterfaces = new Map<string, BrightScriptDeclaration>();
82✔
104

105
    private syncing: Promise<void>;
106
    private encoding: WorkspaceEncoding = new WorkspaceEncoding();
82✔
107

108
    private disposable: Disposable;
109

110
    private onDidChangeEmitter = new EventEmitter<DeclarationChangeEvent>();
82✔
111
    private onDidDeleteEmitter = new EventEmitter<DeclarationDeleteEvent>();
82✔
112
    private onDidResetEmitter = new EventEmitter<void>();
82✔
113

114
    get onDidChange(): Event<DeclarationChangeEvent> {
115
        return this.onDidChangeEmitter.event;
49✔
116
    }
117

118
    get onDidDelete(): Event<DeclarationDeleteEvent> {
119
        return this.onDidDeleteEmitter.event;
49✔
120
    }
121

122
    get onDidReset(): Event<void> {
123
        return this.onDidResetEmitter.event;
48✔
124
    }
125

126
    public async sync(): Promise<void> {
127
        if (this.syncing === undefined) {
×
128
            this.syncing = this.flush().then(() => {
×
129
                this.syncing = undefined;
×
130
            });
131
        }
132
        return this.syncing;
×
133
    }
134

135
    public dispose() {
136
        this.disposable.dispose();
×
137
    }
138

139
    private onDidChangeFile(uri: Uri) {
140
        this.dirty.set(uri.fsPath, uri);
×
141
    }
142

143
    private onDidDeleteFile(uri: Uri) {
144
        this.dirty.delete(uri.fsPath);
×
145
        this.onDidDeleteEmitter.fire(new DeclarationDeleteEvent(uri));
×
146
    }
147

148
    private onDidChangeWorkspace() {
149
        this.fullscan = true;
×
150
        this.dirty.clear();
×
151
        this.encoding.reset();
×
152
        this.onDidResetEmitter.fire();
×
153
    }
154

155
    private async flush(): Promise<void> {
156
        const excludes = getExcludeGlob();
84✔
157

158
        if (this.fullscan) {
84✔
159
            this.fullscan = false;
82✔
160

161
            for (const uri of await vscode.workspace.findFiles('**/*.{brs,bs}', excludes)) {
82✔
162
                this.dirty.set(uri.fsPath, uri);
×
163
            }
164
        }
165
        if (this.dirty.size === 0) {
84✔
166
            return;
82✔
167
        }
168
        for (const [path, uri] of Array.from(this.dirty)) {
2✔
169
            const input = await new Promise<string>((resolve, reject) => {
2✔
170
                fs.readFile(path, (err, data) => {
2✔
171
                    if (err) {
2✔
172
                        if (typeof err === 'object' && err.code === 'ENOENT') {
1!
173
                            resolve(null);
1✔
174
                        } else {
175
                            reject(err);
×
176
                        }
177
                    } else {
178
                        resolve(iconv.decode(data, this.encoding.find(path)));
1✔
179
                    }
180
                });
181
            });
182
            if (input === null) {
2✔
183
                this.dirty.delete(path);
1✔
184
                this.onDidDeleteEmitter.fire(new DeclarationDeleteEvent(uri));
1✔
185
                continue;
1✔
186
            }
187
            if (this.dirty.delete(path)) {
1!
188
                this.onDidChangeEmitter.fire(new DeclarationChangeEvent(uri, this.readDeclarations(uri, input)));
1✔
189
            }
190
        }
191
    }
192

193
    public readDeclarations(uri: Uri, input: string): BrightScriptDeclaration[] {
194
        const uriPath = util.normalizeFileScheme(uri.toString());
4✔
195
        const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
4✔
196
        if (workspaceFolder) {
4✔
197
            const outDir = util.normalizeFileScheme(path.join(workspaceFolder.uri.toString(), 'out'));
3✔
198

199
            // Prevents results in the out directory from being returned
200
            if (uriPath.startsWith(outDir)) {
3!
NEW
201
                return [];
×
202
            }
203
        }
204

205
        const container = BrightScriptDeclaration.fromUri(uri);
4✔
206
        const symbols: BrightScriptDeclaration[] = [];
4✔
207
        let currentFunction: BrightScriptDeclaration;
208
        let funcEndLine: number;
209
        let funcEndChar: number;
210
        let mDefs = {};
4✔
211

212
        let oldNamespaces = this.fileNamespaces.get(uri);
4✔
213
        if (oldNamespaces) {
4!
214
            for (let key of oldNamespaces.keys()) {
×
215
                let ns = this.allNamespaces.get(key);
×
216
                if (ns && ns.uri === uri) {
×
217
                    this.allNamespaces.delete(key);
×
218
                }
219
            }
220
        }
221
        this.fileNamespaces.delete(uri);
4✔
222

223
        let oldEnums = this.fileEnums.get(uri);
4✔
224
        if (oldEnums) {
4!
225
            for (let key of oldEnums.keys()) {
×
226
                let ns = this.allEnums.get(key);
×
227
                if (ns && ns.uri === uri) {
×
228
                    this.allEnums.delete(key);
×
229
                }
230
            }
231
        }
232
        this.fileEnums.delete(uri);
4✔
233

234
        let oldInterfaces = this.fileInterfaces.get(uri);
4✔
235
        if (oldInterfaces) {
4!
236
            for (let key of oldInterfaces.keys()) {
×
237
                let ns = this.allInterfaces.get(key);
×
238
                if (ns && ns.uri === uri) {
×
239
                    this.allInterfaces.delete(key);
×
240
                }
241
            }
242
        }
243
        this.fileInterfaces.delete(uri);
4✔
244

245
        let oldClasses = this.fileClasses.get(uri);
4✔
246
        if (oldClasses) {
4!
247

248
            for (let key of oldClasses.keys()) {
×
249
                let clazz = this.allClasses.get(key);
×
250
                if (clazz && clazz.uri === uri) {
×
251
                    this.allClasses.delete(key);
×
252
                }
253
            }
254
        }
255
        this.fileClasses.delete(uri);
4✔
256

257
        let namespaces = new Set<string>();
4✔
258
        let classes = new Set<string>();
4✔
259
        let enums = new Set<string>();
4✔
260
        let interfaces = new Set<string>();
4✔
261

262
        // Stack of container declarations (namespace/class/interface/enum) that are currently open.
263
        // The innermost open container is the parent for any symbols found inside it, so the outline
264
        // nests correctly. Each container's bodyRange is closed out when its matching `end` is reached.
265
        let containerStack: BrightScriptDeclaration[] = [];
4✔
266
        const currentContainer = () => (containerStack.length > 0 ? containerStack[containerStack.length - 1] : container);
8✔
267
        const closeContainer = (kind: SymbolKind, endLine: number, endChar: number) => {
4✔
268
            const openContainer = containerStack[containerStack.length - 1];
5✔
269
            if (openContainer && openContainer.kind === kind) {
5!
270
                openContainer.bodyRange = openContainer.bodyRange.with({ end: new Position(endLine, endChar) });
5✔
271
                containerStack.pop();
5✔
272
            }
273
        };
274

275
        for (const [line, text] of iterlines(input)) {
4✔
276
            // console.log("" + line + ": " + text);
277
            funcEndLine = line;
16✔
278
            funcEndChar = text.length;
16✔
279

280
            //FUNCTION START
281
            let match = /^\s*(?:public|protected|private)*\s*(?:override)*\s*(?:function|sub)\s+(.*[^\(])\s*\((.*)\)/i.exec(text);
16✔
282
            // console.log("match " + match);
283
            if (match !== null) {
16✔
284
                // function has started
285
                if (currentFunction !== undefined) {
3!
286
                    currentFunction.bodyRange = currentFunction.bodyRange.with({ end: new Position(funcEndLine, funcEndChar) });
×
287
                }
288
                currentFunction = new BrightScriptDeclaration(
3✔
289
                    match[1].trim(),
290
                    match[1].trim().toLowerCase() === 'new' ? SymbolKind.Constructor : SymbolKind.Function,
3!
291
                    currentContainer(),
292
                    match[2].split(','),
293
                    new Range(line, match[0].length - match[1].length - match[2].length - 2, line, match[0].length - 1),
294
                    new Range(line, 0, line, text.length)
295
                );
296
                symbols.push(currentFunction);
3✔
297
                continue;
3✔
298
            }
299

300
            //FUNCTION END
301
            match = /^\s*(end)\s*(function|sub)/i.exec(text);
13✔
302
            if (match !== null) {
13✔
303
                // console.log("function END");
304
                if (currentFunction !== undefined) {
3!
305
                    currentFunction.bodyRange = currentFunction.bodyRange.with({ end: new Position(funcEndLine, funcEndChar) });
3✔
306
                    //reset so the function's range isn't stretched over whatever follows it (e.g. a nested namespace)
307
                    currentFunction = undefined;
3✔
308
                }
309
                continue;
3✔
310
            }
311

312
            // //FIELD
313
            match = /^(?!.*\()(?: |\t)*(public|private|protected)(?: |\t)*([a-z|\.|_]*).*((?: |\t)*=(?: |\t)*.*)*$/i.exec(text);
10✔
314
            if (match !== null) {
10!
315
                // console.log("FOUND VAR " + match);
316
                const name = match[2].trim();
×
317
                if (mDefs[name] !== true) {
×
318
                    mDefs[name] = true;
×
319
                    let varSymbol = new BrightScriptDeclaration(
×
320
                        name,
321
                        SymbolKind.Field,
322
                        currentContainer(),
323
                        undefined,
324
                        new Range(line, match[0].length - match[1].length, line, match[0].length),
325
                        new Range(line, 0, line, text.length)
326
                    );
327
                    // console.log('FOUND VAR ' + varSymbol.name);
328
                    symbols.push(varSymbol);
×
329
                }
330
                continue;
×
331
            }
332

333
            //start namespace declaration
334
            match = /^(?: |\t)*namespace(?: |\t)*([a-z|\.|_]*).*$/i.exec(text);
10✔
335
            if (match !== null) {
10✔
336
                const name = match[1].trim();
5✔
337
                if (name) {
5!
338
                    // For nested/dotted namespaces (e.g. `get.deep`), show only the final
339
                    // segment (`deep`) in the outline, matching the language server behavior.
340
                    const displayName = name.split('.').pop();
5✔
341
                    const namespaceSymbol = new BrightScriptDeclaration(
5✔
342
                        displayName,
343
                        SymbolKind.Namespace,
344
                        currentContainer(),
345
                        undefined,
346
                        new Range(line, match[0].length - match[1].length, line, match[0].length),
347
                        new Range(line, 0, line, text.length)
348
                    );
349
                    // console.log('FOUND NAMESPACES ' + namespaceSymbol.name);
350
                    symbols.push(namespaceSymbol);
5✔
351
                    namespaces.add(name.toLowerCase());
5✔
352
                    containerStack.push(namespaceSymbol);
5✔
353
                }
354
                continue;
5✔
355
            }
356
            //end namespace declaration
357
            match = /^(?: |\t)*end namespace.*$/i.exec(text);
5✔
358
            if (match !== null) {
5!
359
                closeContainer(SymbolKind.Namespace, line, text.length);
5✔
360
                continue;
5✔
361
            }
362

363
            //start enum declaration
364
            match = /^(?: |\t)*enum(?: |\t)*([a-z|\.|_]*).*$/i.exec(text);
×
365
            if (match !== null) {
×
366
                const name = match[1].trim();
×
367
                if (name) {
×
NEW
368
                    const enumSymbol = new BrightScriptDeclaration(
×
369
                        name,
370
                        SymbolKind.Enum,
371
                        currentContainer(),
372
                        undefined,
373
                        new Range(line, match[0].length - match[1].length, line, match[0].length),
374
                        new Range(line, 0, line, text.length)
375
                    );
376
                    // console.log('FOUND enumS ' + enumSymbol.name);
377
                    symbols.push(enumSymbol);
×
378
                    enums.add(name.toLowerCase());
×
NEW
379
                    containerStack.push(enumSymbol);
×
380
                }
NEW
381
                continue;
×
382
            }
383
            //end enum declaration
384
            match = /^(?: |\t)*end enum.*$/i.exec(text);
×
NEW
385
            if (match !== null) {
×
NEW
386
                closeContainer(SymbolKind.Enum, line, text.length);
×
NEW
387
                continue;
×
388
            }
389

390
            //start class declaration
391
            match = /(?:(class)\s+([a-z_][a-z0-9_]*))\s*(?:extends\s*([a-z_][a-z0-9_]+))*$/i.exec(text);
×
392
            if (match !== null) {
×
393
                const name = match[2].trim();
×
394
                if (name) {
×
NEW
395
                    const classSymbol = new BrightScriptDeclaration(
×
396
                        name,
397
                        SymbolKind.Class,
398
                        currentContainer(),
399
                        undefined,
400
                        new Range(line, match[0].length - match[2].length, line, match[0].length),
401
                        new Range(line, 0, line, text.length)
402
                    );
403
                    // console.log('FOUND CLASS ' + classSymbol.name);
404
                    symbols.push(classSymbol);
×
405
                    classes.add(name.toLowerCase());
×
NEW
406
                    containerStack.push(classSymbol);
×
407
                }
NEW
408
                continue;
×
409
            }
410
            //end class declaration
NEW
411
            match = /^(?: |\t)*end class.*$/i.exec(text);
×
NEW
412
            if (match !== null) {
×
NEW
413
                closeContainer(SymbolKind.Class, line, text.length);
×
NEW
414
                continue;
×
415
            }
416

417
            //start interface declaration
418
            match = /(?:(interface)\s+([a-z_][a-z0-9_]*))\s*(?:extends\s*([a-z_][a-z0-9_]+))*$/i.exec(text);
×
419
            if (match !== null) {
×
420
                const name = match[2].trim();
×
421
                if (name) {
×
NEW
422
                    const interfaceSymbol = new BrightScriptDeclaration(
×
423
                        name,
424
                        SymbolKind.Interface,
425
                        currentContainer(),
426
                        undefined,
427
                        new Range(line, match[0].length - match[2].length, line, match[0].length),
428
                        new Range(line, 0, line, text.length)
429
                    );
430
                    // console.log('FOUND interface ' + interfaceSymbol.name);
431
                    symbols.push(interfaceSymbol);
×
432
                    interfaces.add(name.toLowerCase());
×
NEW
433
                    containerStack.push(interfaceSymbol);
×
434
                }
NEW
435
                continue;
×
436
            }
437
            //end interface declaration
NEW
438
            match = /^(?: |\t)*end interface.*$/i.exec(text);
×
NEW
439
            if (match !== null) {
×
NEW
440
                closeContainer(SymbolKind.Interface, line, text.length);
×
NEW
441
                continue;
×
442
            }
443

444
        }
445
        this.fileNamespaces.set(uri, namespaces);
4✔
446
        this.fileClasses.set(uri, classes);
4✔
447
        this.cache.set(uri.fsPath, symbols);
4✔
448
        return symbols;
4✔
449
    }
450

451
    public declToSymbolInformation(uri: Uri, decl: BrightScriptDeclaration): SymbolInformation {
452
        return new SymbolInformation(
×
453
            decl.name,
454
            decl.kind,
455
            decl.containerName ? decl.containerName : decl.name,
×
456
            new Location(uri, decl.bodyRange)
457
        );
458
    }
459

460
    public getFunctionBeforeLine(filePath: string, lineNumber: number): BrightScriptDeclaration | null {
461
        let symbols = this.cache.get(filePath);
×
462
        if (!symbols) {
×
463
            try {
×
464

465
                let uri = vscode.Uri.file(filePath);
×
466
                let decls = this.readDeclarations(uri, fs.readFileSync(filePath, 'utf8'));
×
467
                this.cache.set(filePath, decls);
×
468
                // if there was no match, then get the declarations now
469
                symbols = this.cache.get(filePath);
×
470
            } catch (e) {
471
                console.error(`error loading symbols for file ${filePath}: ${e.message}`);
×
472
            }
473
        }
474
        //try to load it now
475
        if (symbols) {
×
476
            const matchingMethods = symbols
×
477
                .filter((symbol) => symbol.kind === SymbolKind.Function && symbol.nameRange.start.line < lineNumber);
×
478
            return matchingMethods.length > 0 ? matchingMethods[matchingMethods.length - 1] : null;
×
479
        }
480
        return null;
×
481
    }
482

483
}
484

485
export function getExcludeGlob(): string {
1✔
486
    const exclude = [
84✔
487
        ...Object.keys(util.getConfiguration('search').get('exclude') || {}),
168✔
488
        ...Object.keys(util.getConfiguration('files').get('exclude') || {})
168✔
489
    ].join(',');
490
    return `{${exclude}}`;
84✔
491
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc