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

rokucommunity / brighterscript / #13787

31 Jan 2024 09:27PM UTC coverage: 88.178% (-0.2%) from 88.331%
#13787

push

TwitchBronBron
0.65.21

5869 of 7140 branches covered (82.2%)

Branch coverage included in aggregate %.

8698 of 9380 relevant lines covered (92.73%)

1683.86 hits per line

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

88.37
/src/bscPlugin/hover/HoverProcessor.ts
1
import { SourceNode } from 'source-map';
1✔
2
import { isBrsFile, isFunctionType, isXmlFile } from '../../astUtils/reflection';
1✔
3
import type { BrsFile } from '../../files/BrsFile';
4
import type { XmlFile } from '../../files/XmlFile';
5
import type { Hover, ProvideHoverEvent } from '../../interfaces';
6
import type { Token } from '../../lexer/Token';
7
import { TokenKind } from '../../lexer/TokenKind';
1✔
8
import { BrsTranspileState } from '../../parser/BrsTranspileState';
1✔
9
import { ParseMode } from '../../parser/Parser';
1✔
10
import util from '../../util';
1✔
11

12
export class HoverProcessor {
1✔
13
    public constructor(
14
        public event: ProvideHoverEvent
30✔
15
    ) {
16

17
    }
18

19
    public process() {
20
        let hover: Hover | undefined;
21
        if (isBrsFile(this.event.file)) {
30!
22
            hover = this.getBrsFileHover(this.event.file);
30✔
23
        } else if (isXmlFile(this.event.file)) {
×
24
            hover = this.getXmlFileHover(this.event.file);
×
25
        }
26

27
        //if we got a result, "return" it
28
        if (hover) {
30✔
29
            //assign the hover to the event
30
            this.event.hovers.push(hover);
26✔
31
        }
32
    }
33

34
    private buildContentsWithDocs(text: string, startingToken: Token) {
35
        const parts = [text];
20✔
36
        const docs = this.getTokenDocumentation((this.event.file as BrsFile).parser.tokens, startingToken);
20✔
37
        if (docs) {
20✔
38
            parts.push('***', docs);
2✔
39
        }
40
        return parts.join('\n');
20✔
41
    }
42

43
    private getBrsFileHover(file: BrsFile): Hover | undefined {
44
        const scope = this.event.scopes[0];
30✔
45
        const fence = (code: string) => util.mdFence(code, 'brightscript');
30✔
46
        //get the token at the position
47
        let token = file.getTokenAt(this.event.position);
30✔
48

49
        let hoverTokenTypes = [
30✔
50
            TokenKind.Identifier,
51
            TokenKind.Function,
52
            TokenKind.EndFunction,
53
            TokenKind.Sub,
54
            TokenKind.EndSub
55
        ];
56

57
        //throw out invalid tokens and the wrong kind of tokens
58
        if (!token || !hoverTokenTypes.includes(token.kind)) {
30✔
59
            return undefined;
4✔
60
        }
61

62
        const expression = file.getClosestExpression(this.event.position);
26✔
63
        if (expression) {
26!
64
            let containingNamespace = file.getNamespaceStatementForPosition(expression.range.start)?.getName(ParseMode.BrighterScript);
26✔
65
            const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.');
30✔
66

67
            //find a constant with this name
68
            const constant = scope?.getConstFileLink(fullName, containingNamespace);
26!
69
            if (constant) {
26✔
70
                const constantValue = new SourceNode(null, null, null, constant.item.value.transpile(new BrsTranspileState(file))).toString();
5✔
71
                return {
5✔
72
                    contents: this.buildContentsWithDocs(fence(`const ${constant.item.fullName} = ${constantValue}`), constant.item.tokens.const),
73
                    range: token.range
74
                };
75
            }
76
        }
77

78
        let lowerTokenText = token.text.toLowerCase();
21✔
79

80
        //look through local variables first
81
        {
82
            //get the function scope for this position (if exists)
83
            let functionScope = file.getFunctionScopeAtPosition(this.event.position);
21✔
84
            if (functionScope) {
21!
85
                //find any variable with this name
86
                for (const varDeclaration of functionScope.variableDeclarations) {
21✔
87
                    //we found a variable declaration with this token text!
88
                    if (varDeclaration.name.toLowerCase() === lowerTokenText) {
21✔
89
                        let typeText: string;
90
                        if (isFunctionType(varDeclaration.type)) {
6✔
91
                            typeText = varDeclaration.type.toString();
2✔
92
                        } else {
93
                            typeText = `${varDeclaration.name} as ${varDeclaration.type.toString()}`;
4✔
94
                        }
95
                        return {
6✔
96
                            range: token.range,
97
                            //append the variable name to the front for scope
98
                            contents: fence(typeText)
99
                        };
100
                    }
101
                }
102
                for (const labelStatement of functionScope.labelStatements) {
15✔
103
                    if (labelStatement.name.toLocaleLowerCase() === lowerTokenText) {
×
104
                        return {
×
105
                            range: token.range,
106
                            contents: fence(`${labelStatement.name}: label`)
107
                        };
108
                    }
109
                }
110
            }
111
        }
112

113
        //look through all callables in relevant scopes
114
        for (let scope of this.event.scopes) {
15✔
115
            let callable = scope.getCallableByName(lowerTokenText);
15✔
116
            if (callable) {
15!
117
                return {
15✔
118
                    range: token.range,
119
                    contents: this.buildContentsWithDocs(fence(callable.type.toString()), callable.functionStatement?.func?.functionType)
90✔
120
                };
121
            }
122
        }
123
    }
124

125
    /**
126
     * Combine all the documentation found before a token (i.e. comment tokens)
127
     */
128
    private getTokenDocumentation(tokens: Token[], token?: Token) {
129
        const comments = [] as Token[];
20✔
130
        if (!token) {
20✔
131
            return undefined;
3✔
132
        }
133
        const idx = tokens?.indexOf(token);
17!
134
        if (!idx || idx === -1) {
17✔
135
            return undefined;
3✔
136
        }
137
        for (let i = idx - 1; i >= 0; i--) {
14✔
138
            const token = tokens[i];
42✔
139
            //skip whitespace and newline chars
140
            if (token.kind === TokenKind.Comment) {
42✔
141
                comments.push(token);
7✔
142
            } else if (token.kind === TokenKind.Newline || token.kind === TokenKind.Whitespace) {
35✔
143
                //skip these tokens
144
                continue;
25✔
145

146
                //any other token means there are no more comments
147
            } else {
148
                break;
10✔
149
            }
150
        }
151
        if (comments.length > 0) {
14✔
152
            return comments.reverse().map(x => x.text.replace(/^('|rem)/i, '')).join('\n');
7✔
153
        }
154
    }
155

156
    private getXmlFileHover(file: XmlFile): Hover | undefined {
157
        //TODO add xml hovers
158
        return undefined;
×
159
    }
160
}
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