diff --git a/.gitignore b/.gitignore index 0a24e62..02a2f8b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ out node_modules client/server .vscode-test -test*/** \ No newline at end of file +sample*/** +*.vsix \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index dc52bd8..b2ca3b6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,10 @@ "request": "launch", "name": "Launch Client", "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}"], + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}", + "${workspaceFolder}/sample" + ], "outFiles": ["${workspaceRoot}/client/out/**/*.js"], "preLaunchTask": { "type": "npm", diff --git a/.vscode/settings.json b/.vscode/settings.json index 21dc18c..e436cf3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,9 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, - "vbaLanguageServer.trace.server": "verbose" + "vbaLanguageServer.trace.server": "verbose", + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/node_modules/**": true + } } \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore index 382ed7c..65c609c 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,15 +1,36 @@ -.vscode/** -**/*.ts -**/*.map +# Project .gitignore +.eslintignore +.vscode/** +.travis.yml **/tsconfig.json **/tsconfig.base.json contributing.md -.travis.yml + +# Development +**/src + +# # Typescript +**/*.ts +**/*.map + +# Generated +.antlr +*.java +*.interp +*.tokens +*.vsix + +# Samples and testing +sample*/** +**/test/** +**/testFixture/** + +# Client node modules client/node_modules/** !client/node_modules/vscode-jsonrpc/** !client/node_modules/vscode-languageclient/** !client/node_modules/vscode-languageserver-protocol/** !client/node_modules/vscode-languageserver-types/** !client/node_modules/{minimatch,brace-expansion,concat-map,balanced-match}/** -!client/node_modules/{semver,lru-cache,yallist}/** \ No newline at end of file +!client/node_modules/{semver,lru-cache,yallist}/** diff --git a/README.md b/README.md index 2103fa7..2f17abe 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,26 @@ Adds VBA language support to VSCode via LSP compliant Language Server. +![VBA LSP](images/vba-lsp.png) + ## Features + * Syntax highlighting (resolved on client) * Semantic highlighting * Folding ranges * Document symbols -* Diagnostics +## Coming Soon + +* Hovers +* Diagnostics (info, warnings, and errors) ## Installation +* Through the VBA Marketplace, +* VSCode command palette `ext install sslinky.vba-lsp`, or; +* Download the [visx](/releases/latest). + ## Contributing -Contributors welcome! Please see [contributing.md](tbc). \ No newline at end of file +Contributors welcome! Please see [contributing.md](/contributing.md). \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 2523118..3fca6ec 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,11 +1,11 @@ { - "name": "lsp-sample-client", + "name": "vba-lsp-client", "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "lsp-sample-client", + "name": "vba-lsp-client", "version": "0.0.1", "license": "MIT", "dependencies": { diff --git a/client/package.json b/client/package.json index c03dfdd..7215d19 100644 --- a/client/package.json +++ b/client/package.json @@ -7,7 +7,7 @@ "publisher": "SSlinky", "repository": { "type": "git", - "url": "https://github.com/SSlinky/tbc" + "url": "https://github.com/SSlinky/VB-LanguageServer" }, "engines": { "vscode": "^1.63.0" diff --git a/client/src/syntaxes/vba.tmLanguage.yaml b/client/src/syntaxes/vba.tmLanguage.yaml index 41a826d..4b4fe16 100644 --- a/client/src/syntaxes/vba.tmLanguage.yaml +++ b/client/src/syntaxes/vba.tmLanguage.yaml @@ -19,10 +19,10 @@ repository: - include: "#mthdSig" - include: "#variableDeclarations" - include: "#block" - - include: "#testing" + - include: "#testing" repository: moduleLines: - match: "(.*):" + match: '([^\n:]*)?:(?=([^"]*"[^"]*")*[^"]*$)' captures: 1: patterns: @@ -52,8 +52,7 @@ repository: repository: string: name: string.quoted.double.vba - begin: "\"" - end: "\"" + match: '"[^\r\n]*"' boolean: name: constant.language.boolean.vba match: "(?i)(true|false)" diff --git a/contributing.md b/contributing.md new file mode 100644 index 0000000..d6e83d9 --- /dev/null +++ b/contributing.md @@ -0,0 +1,35 @@ +# Contributing + +## Setup + +1. Clone repo +2. Install Java >= 11 +3. Install NPM +4. `npm install` to install dependencies. +5. `npm run textMate` first time and every time grammar is changed. +6. `npm run antlr4ts` first time and every time grammar is changed. +7. (Optional) Install [ANTLR4 grammar syntax support](https://marketplace.visualstudio.com/items?itemName=mike-lischke.vscode-antlr4) VSCode extension. + +## Debugging Language Server + +* Use the Client + Server launch configuration. + +## Debugging Antlr Grammar + +Requires the ANTLR4 grammar syntax support extension installed. Each time you open the workspace, you'll need to open a grammar file to activate the extension. + +Note the grammar file for this project can be found at /server/src/antlr/vba.g4 + +### Show Parse Tree + +* Open the VBA file you want to debug. +* Run `Debug ANTLR4 grammar` from launch configurations. + +### Debug Rule + +* Open the grammar file. +* Right click a rule and choose an option from the context menu. + * Show Railroad Diagram for Rule + * Show ATN Graph for Rule + +The grammar call graph is interesting, but not particularly useful. Generating "valid" input generates garbage. diff --git a/images/vba-lsp-icon.png b/images/vba-lsp-icon.png new file mode 100644 index 0000000..e9ddeda Binary files /dev/null and b/images/vba-lsp-icon.png differ diff --git a/images/vba-lsp.png b/images/vba-lsp.png new file mode 100644 index 0000000..ff1f5e6 Binary files /dev/null and b/images/vba-lsp.png differ diff --git a/package-lock.json b/package-lock.json index 7fe0e56..17fde37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,20 @@ { "name": "vba-lsp", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vba-lsp", - "version": "1.0.0", + "version": "1.1.0", "hasInstallScript": true, "license": "MIT", - "dependencies": { - "antlr4ts": "^0.5.0-alpha.4" - }, "devDependencies": { "@types/mocha": "^9.1.0", "@types/node": "^16.11.7", "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.0", + "antlr4ts": "^0.5.0-alpha.4", "antlr4ts-cli": "^0.5.0-alpha.4", "eslint": "^8.13.0", "js-yaml": "^4.1.0", @@ -451,7 +449,9 @@ "node_modules/antlr4ts": { "version": "0.5.0-alpha.4", "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", - "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==" + "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/antlr4ts-cli": { "version": "0.5.0-alpha.4", @@ -2411,7 +2411,8 @@ "antlr4ts": { "version": "0.5.0-alpha.4", "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", - "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==" + "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", + "dev": true }, "antlr4ts-cli": { "version": "0.5.0-alpha.4", diff --git a/package.json b/package.json index 467d879..e6abbf4 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,16 @@ { "name": "vba-lsp", - "description": "A language server for VBA", + "displayName": "VBA Pro", + "description": "A VBA extension for VSCode with Language Server support", + "icon": "images/vba-lsp-icon.png", "author": "SSlinky", "license": "MIT", - "version": "1.0.0", + "version": "1.1.0", "repository": { "type": "git", - "url": "https://github.com/SSlinky/TBC" + "url": "https://github.com/SSlinky/VBA-LanguageServer" }, - "publisher": "SSlinky", + "publisher": "NotisDataAnalytics", "categories": [ "Programming Languages", "Snippets", @@ -22,14 +24,12 @@ "vscode": "^1.63.0" }, "main": "./client/out/extension", + "activationEvents": [], "contributes": { "languages": [ { "id": "vba", - "aliases": [ - "VBA", - "vba" - ], + "aliases": ["VBA"], "extensions": [ ".bas", ".cls", @@ -38,6 +38,11 @@ "configuration": "./vba.language-configuration.json" } ], + "configurationDefaults": { + "[vba]": { + "editor.semanticHighlighting.enabled": true + } + }, "configuration": { "type": "object", "title": "Default configuration", diff --git a/server/package-lock.json b/server/package-lock.json index b8585d2..c8256ad 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,11 +1,11 @@ { - "name": "lsp-sample-server", + "name": "vba-lsp-server", "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "lsp-sample-server", + "name": "vba-lsp-server", "version": "1.0.0", "license": "MIT", "dependencies": { diff --git a/server/src/antlr/vba.g4 b/server/src/antlr/vba.g4 index e8ca61c..1f3ec80 100644 --- a/server/src/antlr/vba.g4 +++ b/server/src/antlr/vba.g4 @@ -18,10 +18,17 @@ grammar vba; startRule: module EOF; module: - WS? (endOfLine | unknownLine)* (moduleHeader endOfLine*)? moduleConfig? endOfLine* - moduleAttributes? endOfLine* moduleDeclarations? endOfLine* moduleBody? endOfLine* WS?; + WS? moduleHeader + moduleBody? endOfLine* WS?; -moduleHeader: VERSION WS DOUBLELITERAL WS CLASS; +moduleHeader: + (endOfLine | unknownLine)* + (moduleVerson endOfLine*)? + moduleConfig? endOfLine* + moduleAttributes? endOfLine* + moduleDeclarations? endOfLine*; + +moduleVerson: VERSION WS DOUBLELITERAL WS CLASS; moduleConfig: BEGIN endOfLine* moduleConfigElement+ END; @@ -44,14 +51,8 @@ moduleOption: moduleDeclarationsElement: comment | declareStmt - | enumerationStmt - | eventStmt - | constStmt - | implementsStmt // TODO : not valid in a module!!! - | variableStmt + | implementsStmt | moduleOption - | typeStmt - | macroStmt | unknownLine; macroStmt: macroConstStmt | macroIfThenElseStmt; @@ -60,9 +61,14 @@ moduleBody: moduleBodyElement (endOfLine+ moduleBodyElement)* endOfLine*; moduleBodyElement: - methodStmt + constStmt + | enumerationStmt + | eventStmt + | macroStmt + | methodStmt | propertyStmt - | macroStmt; + | typeStmt + | variableStmt; // block ---------------------------------- diff --git a/server/src/capabilities/diagnostics.ts b/server/src/capabilities/diagnostics.ts new file mode 100644 index 0000000..1b552c5 --- /dev/null +++ b/server/src/capabilities/diagnostics.ts @@ -0,0 +1,6 @@ +import { TextDocumentClientCapabilities } from 'vscode-languageserver'; + + +function hasDiagnosticRelatedInformationCapability(x: TextDocumentClientCapabilities) { + return !!(x && x.publishDiagnostics && x.publishDiagnostics.relatedInformation); +} \ No newline at end of file diff --git a/server/src/capabilities/folding.ts b/server/src/capabilities/folding.ts new file mode 100644 index 0000000..da503c2 --- /dev/null +++ b/server/src/capabilities/folding.ts @@ -0,0 +1,57 @@ +import { FoldingRange as LSFoldingRange } from 'vscode-languageserver'; +import { FoldableElement } from '../project/elements/special'; + +/** + * Enum of known range kinds + * ~~ https://github.com/microsoft/vscode-languageserver-protocol-foldingprovider/blob/master/protocol.foldingProvider.md + */ +export enum FoldingRangeKind { + /** + * Folding range for a comment + */ + Comment = 'comment', + /** + * Folding range for a imports or includes + */ + Imports = 'imports', + /** + * Folding range for a region (e.g. `#region`) + */ + Region = 'region' +} + + +export class FoldingRange implements LSFoldingRange { + /** + * The zero-based line number from where the folded range starts. + */ + startLine: number; + + /** + * The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. + */ + startCharacter?: number; + + /** + * The zero-based line number where the folded range ends. + */ + endLine: number; + + /** + * The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. + */ + endCharacter?: number; + + /** + * Describes the kind of the folding range such as 'comment' or 'region'. The kind + * is used to categorize folding ranges and used by commands like 'Fold all comments'. See + * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + */ + kind?: string; + + constructor(element: FoldableElement, foldingRangeKind?: FoldingRangeKind) { + this.startLine = element.range.start.line; + this.endLine = element.range.end.line; + this.kind = foldingRangeKind; + } +} \ No newline at end of file diff --git a/server/src/capabilities/semanticTokens.ts b/server/src/capabilities/semanticTokens.ts new file mode 100644 index 0000000..4bca659 --- /dev/null +++ b/server/src/capabilities/semanticTokens.ts @@ -0,0 +1,125 @@ +import {InitializeResult, Range, SemanticTokenModifiers, SemanticTokenTypes, uinteger, _LanguagesImpl, SemanticTokens, SemanticTokensParams, SemanticTokensRangeParams} from 'vscode-languageserver'; +import { HasSemanticToken } from '../project/elements/base'; + +const registeredTokenTypes = new Map((Object.keys(SemanticTokenTypes) as (keyof typeof SemanticTokenTypes)[]).map((k, i) => ([k, i]))); +const registeredTokenModifiers = new Map((Object.keys(SemanticTokenModifiers) as (keyof typeof SemanticTokenModifiers)[]).map((k, i) => ([k, 2**i]))); + +export function activateSemanticTokenProvider(result: InitializeResult) { + result.capabilities.semanticTokensProvider = { + "full": true, + legend: { + tokenTypes: Array.from(registeredTokenTypes.keys()), + tokenModifiers: Array.from(registeredTokenModifiers.keys()), + } + }; +} + +export function sortSemanticTokens(tokens: SemanticToken[]): SemanticToken[] { + return tokens.sort((a, b) => { + // Sort a before than b. + if ((a.line < b.line) || (a.line === b.line && a.char < b.char)) + return -1; + // Sort b before a. + if ((a.line > b.line) || (a.line === b.line && a.char > b.char)) + return 1; + // No difference. + return 0; + }); +} + +export class SemanticToken { + line: uinteger; + char: uinteger; + length: uinteger; + tokenType: uinteger; + tokenModifiers: uinteger = 0; + element: HasSemanticToken; + + constructor(element: HasSemanticToken, line: uinteger, startChar: uinteger, length: uinteger, tokenType: SemanticTokenTypes, tokenModifiers: SemanticTokenModifiers[]) { + this.element = element; + this.line = line; + this.char = startChar; + this.length = length; + this.tokenType = registeredTokenTypes.get(tokenType)!; + tokenModifiers.forEach((x) => this.tokenModifiers += registeredTokenModifiers.get(x) ?? 0); + } + + static create(element: HasSemanticToken): SemanticToken { + return new SemanticToken( + element, + element.identifier.range.start.line, + element.identifier.range.start.character, + element.identifier.context.text.length, + element.tokenType, + element.tokenModifiers + ); + } + + toNewRange(range: Range): SemanticToken { + const token = new SemanticToken( + this.element, + range.start.line, + range.start.character, + this.length, + SemanticTokenTypes.class, + [] + ); + token.tokenType = this.tokenType; + token.tokenModifiers = this.tokenModifiers; + return token; + } + + toDeltaToken(line: uinteger = 0, startChar: uinteger = 0): uinteger[] { + const deltaLine = this.line - line; + const deltaChar = deltaLine === 0 ? this.char - startChar : this.char; + return [deltaLine, deltaChar, this.length, this.tokenType, this.tokenModifiers]; + } + + toSemanticTokensArray(reference?: SemanticToken): uinteger[] { + const line = this.line - (reference?.line ?? 0); + const char = line === 0 ? this.char - reference!.char : this.char; + return [ + line, + char, + this.length, + this.tokenType, + this.tokenModifiers + ]; + } +} + + +/** + * Tracks, sorts, and provides LSP response for SemanticTokens. + */ +export class SemanticTokensManager { + private _tokens: SemanticToken[] = []; + + private _tokensInRange = (range: Range) => + this._tokens.filter(token => token.element.isChildOf(range)); + + add(element: HasSemanticToken) { + this._tokens.push(SemanticToken.create(element)); + } + + // getSemanticTokens(params: SemanticTokensParams): SemanticTokens | null; + // getSemanticTokens(rangeParams: SemanticTokensRangeParams): SemanticTokens | null; + // getSemanticTokens(_?: SemanticTokensParams, rangeParams?: SemanticTokensRangeParams): SemanticTokens | null { + getSemanticTokens(range?: Range): SemanticTokens | null { + // Get the range if we have one. + // const range: Range | undefined = rangeParams?.range; + + // Filter and sort the semantic tokens. + const filteredTokens = range ? this._tokensInRange(range) : this._tokens; + const sortedTokens = sortSemanticTokens(filteredTokens); + if (sortedTokens.length === 0) + return null; + + // Get an array of SemanticTokens relative to previous token. + const packedData: uinteger[][] = sortedTokens.map((token, i) => + token.toSemanticTokensArray(i === 0 ? undefined : sortedTokens[i - 1])); + + // Return the flattened array. + return { data: ([] as uinteger[]).concat(...packedData) }; + } +} diff --git a/server/src/capabilities/symbolInformation.ts b/server/src/capabilities/symbolInformation.ts new file mode 100644 index 0000000..bf94b5c --- /dev/null +++ b/server/src/capabilities/symbolInformation.ts @@ -0,0 +1,15 @@ +import { SymbolInformation, SymbolKind } from 'vscode-languageserver'; +import { NamedSyntaxElement } from '../project/elements/base'; + + +export class SymbolInformationFactory { + static create(element: NamedSyntaxElement, symbolKind: SymbolKind): SymbolInformation { + return SymbolInformation.create( + element.name, + symbolKind, + element.range, + element.uri + ); + } +} + diff --git a/server/src/capabilities/vbaSemanticTokens.ts b/server/src/capabilities/vbaSemanticTokens.ts deleted file mode 100644 index 13fe7ff..0000000 --- a/server/src/capabilities/vbaSemanticTokens.ts +++ /dev/null @@ -1,62 +0,0 @@ -import {InitializeResult, Range, SemanticTokenModifiers, SemanticTokenTypes, uinteger, _LanguagesImpl} from 'vscode-languageserver'; - -const tokenTypes = new Map((Object.keys(SemanticTokenTypes) as (keyof typeof SemanticTokenTypes)[]).map((k, i) => ([k, i]))); -const tokenModifiers = new Map((Object.keys(SemanticTokenModifiers) as (keyof typeof SemanticTokenModifiers)[]).map((k, i) => ([k, 2**i]))); - -export function activateSemanticTokenProvider(result: InitializeResult) { - result.capabilities.semanticTokensProvider = { - "full": true, - legend: { - tokenTypes: Array.from(tokenTypes.keys()), - tokenModifiers: Array.from(tokenModifiers.keys()), - } - }; -} - -export function sortSemanticTokens(toks: SemanticToken[]): SemanticToken[] { - return toks.sort((a, b) => { - // Sort a before than b. - if ((a.line < b.line) || (a.line === b.line && a.startChar < b.startChar)) - return -1; - // Sort b before a. - if ((a.line > b.line) || (a.line === b.line && a.startChar > b.startChar)) - return 1; - // No difference. - return 0; - }); -} - -export class SemanticToken { - line: uinteger; - startChar: uinteger; - length: uinteger; - tokenType: uinteger; - tokenModifiers: uinteger = 0; - - constructor(line: uinteger, startChar: uinteger, length: uinteger, tokenType: SemanticTokenTypes, tokenModifier: SemanticTokenModifiers[]) { - this.line = line; - this.startChar = startChar; - this.length = length; - this.tokenType = tokenTypes.get(tokenType)!; - tokenModifier.forEach((x) => this.tokenModifiers += tokenModifiers.get(x) ?? 0); - } - - toNewRange(range: Range): SemanticToken { - const token = new SemanticToken( - range.start.line, - range.start.character, - this.length, - SemanticTokenTypes.class, - [] - ); - token.tokenType = this.tokenType; - token.tokenModifiers = this.tokenModifiers; - return token; - } - - toDeltaToken(line: uinteger = 0, startChar: uinteger = 0): uinteger[] { - const deltaLine = this.line - line; - const deltaChar = deltaLine === 0 ? this.startChar - startChar : this.startChar; - return [deltaLine, deltaChar, this.length, this.tokenType, this.tokenModifiers]; - } -} diff --git a/server/src/capabilities/workspaceFolder.ts b/server/src/capabilities/workspaceFolder.ts new file mode 100644 index 0000000..d42d09c --- /dev/null +++ b/server/src/capabilities/workspaceFolder.ts @@ -0,0 +1,20 @@ +import { ClientCapabilities, InitializeResult } from 'vscode-languageserver'; +import { LanguageServerConfiguration } from '../server'; + + +export function hasConfigurationCapability(languageServerConfiguration: LanguageServerConfiguration) { + const capabilities = languageServerConfiguration.params.capabilities; + return !!(capabilities.workspace && !!capabilities.workspace.configuration); +} + +function hasWorkspaceFolderCapability(x: ClientCapabilities) { + return !!(x.workspace && !!x.workspace.workspaceFolders); +} + +export function activateWorkspaceFolderCapability(capabilities: ClientCapabilities, result: InitializeResult) { + if (hasWorkspaceFolderCapability(capabilities)) { + result.capabilities.workspace = { + workspaceFolders: { supported: true } + }; + } +} diff --git a/server/src/docInfo.ts b/server/src/docInfo.ts deleted file mode 100644 index 9d76a4c..0000000 --- a/server/src/docInfo.ts +++ /dev/null @@ -1,577 +0,0 @@ -import { TextDocument } from 'vscode-languageserver-textdocument'; - -import { MethodElement } from './parser/elements/method'; -import { ModuleElement } from './parser/elements/module'; -import { sortSemanticTokens } from './capabilities/vbaSemanticTokens'; -import { sleep, rangeIsChildOfElement } from './utils/helpers'; -import { FoldableElement, SyntaxElement } from './parser/elements/base'; -import { ResultsContainer, SyntaxParser } from './parser/vbaSyntaxParser'; -import { VariableAssignElement, VariableDeclarationElement, VariableStatementElement } from './parser/elements/variable'; - -import { CompletionItem, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DidChangeConfigurationParams, DidChangeWatchedFilesParams, DocumentSymbol, FoldingRange, Hover, HoverParams, Location, NotificationHandler, Position, PublishDiagnosticsParams, Range, SemanticTokenModifiers, SemanticTokens, SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRequest, SymbolInformation, SymbolKind, TextDocumentPositionParams, TextDocuments, uinteger, _Connection } from 'vscode-languageserver'; - -declare global { - interface Map { - has

(key: P): this is { get(key: P): V } & this - } -} - -interface ExampleSettings { - maxNumberOfProblems: number; -} - - -enum NameLinkType { - 'variable' = 0, - 'method' = 1, -} - -const defaultSettings: ExampleSettings = { maxNumberOfProblems: 1000 }; -let globalSettings: ExampleSettings = defaultSettings; - -export class ProjectInformation { - readonly conn: _Connection; - readonly docs: TextDocuments = new TextDocuments(TextDocument); - readonly docInfos: Map = new Map(); - readonly docSettings: Map> = new Map(); - - private hasConfigurationCapability = false; - private hasWorkspaceFolderCapability = false; - private hasDiagnosticRelatedInformationCapability = false; - - private readonly syntaxUtil: SyntaxParser; - private readonly scopes: Scope = new Scope(); - - constructor(conn: _Connection, hasCfg: boolean, hasWsFld: boolean, hasDiag: boolean) { - this.conn = conn; - this.syntaxUtil = new SyntaxParser(); - this.hasConfigurationCapability = hasCfg; - this.hasWorkspaceFolderCapability = hasWsFld; - this.hasDiagnosticRelatedInformationCapability = hasDiag; - this.addEventHandlers(); - - this.docs.listen(conn); - } - - - onDidChangeConfiguration: NotificationHandler = (change => { - if (this.hasConfigurationCapability) { - this.docSettings.clear(); - } else { - globalSettings = ( - (change.settings.languageServerExample || defaultSettings) - ); - } - - // Revalidate all open text documents - // this.docs.all().forEach(validateTextDocument); - }); - - onDidChangeWatchedFiles: NotificationHandler = (params) => { - this.conn.console.log(`onDidChangeWatchedFiles: ${params}`); - }; - - getFoldingRanges(docUri: string) { - return this.docInfos.get(docUri)?.getFoldingRanges() ?? []; - } - - async getDocumentSymbols(docUri: string): Promise { - const docInfo = this.docInfos.get(docUri); - if (docInfo) { - while (docInfo.isBusy) { - await sleep(50); - } - return docInfo!.getSymbols(docUri); - } - return []; - } - - // TODO: Implement - getCompletion(params: TextDocumentPositionParams) { - return []; - } - - // TODO: Implement - getCompletionResolve(item: CompletionItem): CompletionItem { - return item; - } - - getHover({ textDocument, position }: HoverParams): Hover { - return this.docInfos.get(textDocument.uri)?.getHover(position) ?? { contents: '' }; - } - - getSemanticTokens(f: SemanticTokensParams): SemanticTokens | null; - getSemanticTokens(r: SemanticTokensRangeParams): SemanticTokens | null; - getSemanticTokens(f?: SemanticTokensParams, r?: SemanticTokensRangeParams): SemanticTokens | null { - const range = r?.range; - const uri = f?.textDocument?.uri ?? r?.textDocument?.uri ?? ''; - return this.docInfos.get(uri)?.getSemanticTokens(range) ?? null; - } - - sendDiagnostics = (docInfo: DocumentInformation) => - this.conn.sendDiagnostics(docInfo.getDiagnostics()); - - private getDocumentSettings(docUri: string): Thenable { - if (!this.hasConfigurationCapability) { - return Promise.resolve(globalSettings); - } - let result = this.docSettings.get(docUri); - if (!result) { - result = this.conn.workspace.getConfiguration({ - scopeUri: docUri, - section: 'vbaLanguageServer' - }); - this.docSettings.set(docUri, result); - } - return result; - } - - private async addEventHandlers() { - this.docs.onDidClose(e => { - this.docSettings.delete(e.document.uri); - }); - - this.docs.onDidOpen(async e => { - const uri = e.document.uri; - const stg = await this.getDocumentSettings(uri); - this.docSettings.set(uri, Promise.resolve(stg)); - }); - - this.docs.onDidChangeContent(change => { - const doc = change.document; - const docInfo = new DocumentInformation(this.scopes, doc.uri); - this.scopes.getScope(`undeclared|${doc.uri}`).clear(); - this.docInfos.set(doc.uri, docInfo); - this.syntaxUtil.parse(doc, docInfo); - docInfo.finalise(); - this.sendDiagnostics(docInfo); - }); - } -} - -export class DocumentInformation implements ResultsContainer { - module?: ModuleElement; - elements: SyntaxElement[] = []; - attrubutes: Map = new Map(); - foldingRanges: FoldingRange[] = []; - isBusy = true; - - private docUri: string; - private ancestors: SyntaxElement[] = []; - private documentScope: Scope; - - constructor(scope: Scope, docUri: string) { - this.docUri = docUri; - scope.links.set(docUri, new Map()); - this.docUri = docUri; - this.documentScope = scope; - } - - addModule(emt: ModuleElement) { - this.module = emt; - this.elements.push(emt); - this.ancestors.push(emt); - } - - addFoldingRange(emt: FoldableElement) { - this.foldingRanges.push(emt.foldingRange()!); - } - - addElement(emt: SyntaxElement) { - // Find the parent element. - while (this.ancestors) { - const pnt = this.ancestors.pop()!; - if (emt.isChildOf(pnt)) { - emt.parent = pnt; - pnt.children.push(emt); - this.ancestors.push(pnt); - this.ancestors.push(emt); - break; - } - } - - // Add the element. - this.elements.push(emt); - this.ancestors.push(emt); - - // Also add identifier elements - if (emt.identifier) { - this.addElement(emt.identifier); - // emt.fqName = `${(emt.fqName ?? '')}.${emt.identifier.text}`; - } - - return this; - } - - /** - * Use this method to set as the current scope. - * @param emt The function, sub, or property to scope to. - */ - pushScopeElement(emt: MethodElement) { - this.addScopedDeclaration(emt); - } - - popScopeElement() { - // - } - - addScopedDeclaration(emt: MethodElement | VariableDeclarationElement) { - // Add a declared scope. - const elId = emt.identifier!.text; - const link = this.getNameLink(elId, emt.parent?.namespace ?? '', emt.hasPrivateModifier); - link.declarations.push(emt); - - // Check the undeclared links and merge if found. - const undeclaredScope = this.scope.getScope(`undeclared|${this.docUri}`); - const undeclaredLink = undeclaredScope.get(elId); - if (undeclaredLink) { - link.merge(undeclaredLink); - undeclaredScope.delete(elId); - } - } - - // addScopeReference(emt: VariableAssignElement) { - // const link = this.getNameLink(emt.identifier!.text, emt.parent?.namespace ?? '', false, true); - // link.references.push(emt); - // } - - /** - * Creates scope references for the left and right sides - * of the variable assignment if they exist. - * @param emt the variable assignment element. - */ - addScopeReferences(emt: VariableAssignElement) { - throw new Error("Not implemented exception"); - } - - private getNameLink(identifier: string, fqName: string, isPrivate = false, searchScopes = false): NameLink { - const targetScope = searchScopes ? this.searchScopes(identifier) : this.getScope(fqName, isPrivate); - - // Return an existing link. - if (targetScope.has(identifier)) { - return targetScope.get(identifier); - } - - // Create a new link and return it. - const link = new NameLink(); - targetScope.set(identifier, link); - return link; - } - - private getScope(fqName: string, isPrivate: boolean): Map { - const globalScope = this.scope.getScope('global'); - const localScope = this.scope.getScope(this.docUri); - - const isAtModuleLevel = !(fqName ?? '').includes('.'); - return (isAtModuleLevel && !isPrivate) ? globalScope : localScope; - } - - private searchScopes(identifier: string): Map { - const scopeNames = [this.docUri, 'global']; - for (let i = 0; i < scopeNames.length; i++) { - const scope = this.scope.getScope(scopeNames[i]); - if (scope.has(identifier)) { - return scope; - } - } - return this.scope.getScope(`undeclared|${this.docUri}`); - } - - - finalise() { - // TODO: Intelligently pass opt. explicit. - this.documentScope.processLinks(this.docUri, true); - this.isBusy = false; - } - - // setModuleAttribute = (attr: ModuleAttribute) => - // this.attrubutes.set(attr.key(), attr.value()); - - // setModuleIdentifier(ctx: LiteralContext, doc: TextDocument) { - // if (this.module) - // this.module.identifier = new IdentifierElement(ctx, doc); - // } - - getHover = (p: Position) => - this.getElementAtPosition(p)?.hover(); - - getSemanticTokens(range?: Range): SemanticTokens | null { - const r = range ?? this.module?.range; - if (!r) - return null; - - const semanticTokens = sortSemanticTokens( - this.getElementsInRange(r) - .filter((e) => !!e.identifier?.semanticToken) - .map((e) => e.identifier!.semanticToken!)); - - if (semanticTokens.length === 0) - return null; - - let data: uinteger[] = []; - let line = 0; - let char = 0; - semanticTokens.forEach((t) => { - data = data.concat(t.toDeltaToken(line, char)!); - line = t.line; - char = t.startChar; - }); - return { data: data }; - } - - private getElementAtPosition(p: Position): SyntaxElement | undefined { - const r = Range.create(p, p); - - // Filter eligible parents by range. - let parents = this.elements.filter((x) => rangeIsChildOfElement(r, x)); - if (parents.length === 0) { return; } - if (parents.length === 1) { console.log(`hover@${r.toString()}: ${parents[0].identifier?.text}`); return parents[0]; } - - // Narrow parents down to the one(s) with the narrowest row scope. - // In the incredibly unlikely case that we have two parents with the same number of rows - // and they span more than one row, then it's all too hard. Just pick one. - const minRows = Math.min(...parents.map((x) => x.range.end.line - x.range.start.line)); - parents = parents.filter((x) => x.range.end.line - x.range.start.line === minRows); - if (parents.length === 1 || minRows > 0) { - console.log(`hover@${this.rangeAddress(r)}: ${parents[0].toString()}`); - return parents[0]; - } - - // Narrow parents down to the one with the narrowest character scope. - const minChars = Math.min(...parents.map((x) => x.range.end.character - x.range.start.character)); - parents = parents.filter((x) => x.range.end.character - x.range.start.character === minChars); - return parents[0]; - } - - private getElementsInRange(r: Range): SyntaxElement[] { - const results: SyntaxElement[] = []; - if (r.start.line === r.end.line) { - this.elements.forEach((x) => { - if (x.range.start.character >= r.start.character && x.range.end.character <= r.end.character) - results.push(x); - }); - } else { - this.elements.forEach((x) => { - if (x.range.start.line >= r.start.line && x.range.end.line <= r.end.line) - results.push(x); - }); - } - - return results; - } - - getDiagnostics(): PublishDiagnosticsParams { - return { uri: this.docUri, diagnostics: this.elements.map((e) => e.diagnostics).flat(1) }; - } - - getSymbols = (uri: string): SymbolInformation[] => - this.elements - .filter((x) => (!!x.identifier) && (x.identifier.text !== '')) - .map((x) => x.symbolInformation(uri)) - .filter((x): x is SymbolInformation => !!x); - - // getFoldingRanges = (): (FoldingRange)[] => - // this.elements - // .filter((x) => !(x instanceof ModuleElement)) - // .map((x) => x.foldingRange()) - // .filter((x): x is FoldingRange => !!x); - - getFoldingRanges = (): (FoldingRange)[] => - this.foldingRanges; - - private rangeAddress(r: Range): string { - const sl = r.start.line; - const el = r.end.line; - const sc = r.start.character; - const ec = r.end.character; - - if(sl==el) { - return `${sl}:${sc}-${ec}`; - } - return `${sl}:${sc}-${el}:${ec}`; - } -} - -class Scope { - // { docUri: { identifier: [ declarationElement, elementLink... ] } } - links: Map> = new Map(); - private subScopes: string[] = []; - private currentDoc = ''; - - constructor() { - this.links.set('global', new Map()); - } - - /** - * Gets the scope related to the key. Lazy instantiates. - * @param key the key of the scope to get. - * @returns a Scope. - */ - getScope(key: string): Map { - if (key !== this.currentDoc) { - this.currentDoc = key; - this.subScopes = ['module']; - } - if (!this.links.has(key)) { - this.links.set(key, new Map()); - } - return this.links.get(key)!; - } - - pushSubScope(namespace: string) { - this.subScopes.push(namespace); - } - - popSubScope() { - this.subScopes.pop(); - } - - processLinks(key: string, optExplicit = false) { - // TODO: check global for undeclareds - // TODO: implement explicit paths, e.g. Module1.MyVar - const undeclared = this.getScope(`undeclared|${key}`); - const docScope = this.getScope(key); - undeclared.forEach((v, k) => docScope.set(k, v)); - docScope.forEach((x) => x.process(optExplicit)); - } -} - -class NameLink { - type: NameLinkType = NameLinkType.variable; - - // The original decalarations that affect this name. - // 0: Variable or method not declared. - // 1: Declared once. - // 2: Multiple conflicting declarations. - private _declarations: SyntaxElement[] = []; - declarations: SyntaxElement[] = []; - - // The places this name is referenced. - references: SyntaxElement[] = []; - diagnostics: Diagnostic[] = []; - - private diagnosticRelatedInfo: DiagnosticRelatedInformation[] = []; - - merge(link: NameLink) { - this.declarations.concat(link.declarations); - this.references.concat(link.references); - this.diagnostics.concat(link.diagnostics); - - if (link.declarations.length > 0) { - this.type = link.type; - } - } - - process(optExplicit = false) { - this.addDeclarationReferences(); - this.processDiagnosticRelatedInformation(); - this.validateDeclarationCount(optExplicit); - this.validateMethodSignatures(); - - this.assignSemanticTokens(); - this.assignDiagnostics(); - } - - private addDeclarationReferences() { - this.references.forEach((x) => this.addDecToRef(x)); - } - - private addDecToRef(ref: SyntaxElement) { - if(!(ref instanceof VariableStatementElement)) { - return; - } - const dec = this.declarations[0]; - if(dec instanceof MethodElement) { - ref.setDeclaredType(dec); - } - } - - private processDiagnosticRelatedInformation() { - this.diagnosticRelatedInfo = this.declarations - .concat(this.references) - .map((x) => DiagnosticRelatedInformation.create( - x.location(), - x.text - )); - } - - private validateDeclarationCount(optExplicit: boolean) { - if (this.declarations.length === 1) { - return; - } - - const undecSeverity = (optExplicit) ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning; - if (this.declarations.length === 0) { - if (optExplicit || this.type !== NameLinkType.variable) - this.references.forEach((x) => - this.diagnostics.push(Diagnostic.create( - x.range, - "No declaration for variable or method.", - undecSeverity, - 404, - 'VbaPro', - this.diagnosticRelatedInfo - ))); - return; - } - this.declarations.forEach((x) => - this.diagnostics.push(Diagnostic.create( - x.range, - "Ambiguous variable declaration", - DiagnosticSeverity.Error, - 500, - 'VbaPro', - this.diagnosticRelatedInfo - ))); - } - - private assignSemanticTokens() { - if (this.declarations.length === 0) { - return; - } - - this.references.forEach((x) => x.semanticToken = - this.declarations[0] - .semanticToken - ?.toNewRange(x.range)); - } - - private assignDiagnostics() { - if (this.diagnostics.length === 0) { - return; - } - const els = this.declarations.concat(this.references); - els.forEach((x) => x.addDiagnostics(this.diagnostics)); - } - - private validateMethodSignatures() { - // TODO: implement. - } -} - -class Scope2 { - context: SyntaxElement; - parent?: Scope2; - nameRefs: Map = new Map(); - - constructor(ctx: SyntaxElement); - constructor(ctx: SyntaxElement, pnt: Scope2); - constructor(ctx: SyntaxElement, pnt?: Scope2) { - this.context = ctx; - this.parent = pnt; - } - - addRef(ctx: IdentifiableSyntaxElement) { - const nameLink = this.getName(ctx.identifier.text); - // if dec - - // if not dec - } - - getName(identifier: string): NameLink { - if (!this.nameRefs.has(identifier)) { - this.nameRefs.set(identifier, new NameLink()); - } - return this.nameRefs.get(identifier)!; - } -} \ No newline at end of file diff --git a/server/src/extensions/parserExtensions.ts b/server/src/extensions/parserExtensions.ts new file mode 100644 index 0000000..6b4e305 --- /dev/null +++ b/server/src/extensions/parserExtensions.ts @@ -0,0 +1,90 @@ +import { Range, SymbolKind } from 'vscode-languageserver'; +import { BaseTypeContext, ComplexTypeContext } from '../antlr/out/vbaParser'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +// import { ParserRuleContext } from 'antlr4ts'; + +// This extension throws a compiler error TS2693: 'ParserRuleContext' only refers to a type, but is being used as a value here. +// Can maybe review later down the track, but for now have just made it a private method on BaseSyntaxElement. +// declare module 'antlr4ts' { +// export interface ParserRuleContext { +// toRange(document: TextDocument): Range; +// } +// } + +// ParserRuleContext.prototype.toRange = function (document: TextDocument): Range { +// const startIndex = this.start.startIndex; +// const stopIndex = this.stop?.stopIndex ?? startIndex; +// return Range.create( +// document.positionAt(startIndex), +// document.positionAt(stopIndex) +// ); +// }; + + +declare module '../antlr/out/vbaParser' { + export interface BaseTypeContext { + toSymbolKind(): SymbolKind; + } + + export interface ComplexTypeContext { + toSymbolKind(): SymbolKind; + } +} + +BaseTypeContext.prototype.toSymbolKind = function (): SymbolKind { + return toSymbolKind(this); +}; + +ComplexTypeContext.prototype.toSymbolKind = function (): SymbolKind { + return toSymbolKind(this); +}; + +function toSymbolKind(context: BaseTypeContext | ComplexTypeContext): SymbolKind { + switch (context.text.toLocaleLowerCase()) { + case 'boolean': + return SymbolKind.Boolean; + case 'byte': + case 'string': + return SymbolKind.String; + case 'double': + case 'currency': + case 'integer': + case 'long': + case 'longPtr': + case 'longLong': + return SymbolKind.Number; + case 'object': + return SymbolKind.Object; + default: + return SymbolKind.Class; + } +} + +/** + * const File: 1; + const Module: 2; + const Namespace: 3; + const Package: 4; + const Class: 5; + const Method: 6; + const Property: 7; + const Field: 8; + const Constructor: 9; + const Enum: 10; + const Interface: 11; + const Function: 12; + const Variable: 13; + const Constant: 14; + const String: 15; + const Number: 16; + const Boolean: 17; + const Array: 18; + const Object: 19; + const Key: 20; + const Null: 21; + const EnumMember: 22; + const Struct: 23; + const Event: 24; + const Operator: 25; + const TypeParameter: 26; + */ \ No newline at end of file diff --git a/server/src/extensions/stringExtensions.ts b/server/src/extensions/stringExtensions.ts new file mode 100644 index 0000000..24caf61 --- /dev/null +++ b/server/src/extensions/stringExtensions.ts @@ -0,0 +1,12 @@ +import '.'; + +declare global { + export interface String { + stripQuotes(): string; + } +} + +String.prototype.stripQuotes = function (): string { + const exp = /^"?(.*?)"?$/; + return exp.exec(this.toString())![1]; +}; diff --git a/server/src/parser/document/scope.jsonc b/server/src/parser/document/scope.jsonc deleted file mode 100644 index 4ea4217..0000000 --- a/server/src/parser/document/scope.jsonc +++ /dev/null @@ -1,51 +0,0 @@ -{ - // Represents a project - "markdoc": { - "global": {}, - "LexerMarkdown": { - "global": { - "mTokens": "VariableDeclarationsElement", - "Tokenize": "SubDeclarationElement", - "TokenFactory": "FunctionDeclarationElement", - "Tokens": "PropertyDeclarationElement" - }, - "Tokens": { - "get": {}, - "set": { // no need to distinguish let here. - "var": "VariableDeclarationsElement" - } - } - } - }, - // Similar to a project, available for anything. - // This probably needs to legit be a json resource. - "excelVbaObjectModel": { - "Range": { - "functions": { - "SpecialCells": { - "doc": "Returns a **Range** object that represents all the cells that match the specified type and value.", - "args": { - "Type": "XlCellType", - "[Value]": "Variant" - }, - "returns": "Excel.Range" - } - } - } - }, - "vbaObjectModel": { - "Collection": { - "subs": { - "Add": { - "doc": "Adds a member to a **Collection** object.", - "args": { - "Item": "Variant", - "[Key]": "String", - "[Before]": "Long", - "[After]": "Long" - } - } - } - } - } -} \ No newline at end of file diff --git a/server/src/parser/document/scope.ts b/server/src/parser/document/scope.ts deleted file mode 100644 index 432adbf..0000000 --- a/server/src/parser/document/scope.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { BaseDeclaration } from '../elements/memory/base'; - - -export class ScopeManager { - language = ""; - - - /** - * Initialises the VBA scopes at the language and application level. - * @returns A scope with language and application initialised. - */ - initialiseScope(): Scope { - return new VbaScope(); - } -} - - -/** - * A scope represents a region of code that has a scope. - * This can be a module or a method. - */ -class Scope { - namespace: string; - parent: Scope | undefined; - - constructor(namespace: string, parent: Scope | undefined) { - this.namespace = namespace; - this.parent = parent; - } -} - -class VbaScope extends Scope { - globals: Map = new Map(); - - constructor() { - super("vba", undefined); - } -} - -export class ScopeTable { - // Items listed by their fully qualified name. - definitions: Map = new Map(); - - findDefinition(identifier:string, context: string) - : ScopeTableItem | undefined { - - // Check fqns - if(this.definitions.has(identifier)) { - return this.definitions.get(identifier)[0]; - } - - // Check the project scope. - let result = this.findDef(identifier, context); - if(result) { return result; } - - // Check the application scope. - result = this.findDef(identifier, 'Application'); - if(result) { return result; } - - // Check the language scope. - result = this.findDef(identifier, 'Vba'); - if(result) { return result; } - } - - private findDef(identifier:string, context: string) - : ScopeTableItem | undefined { - const names = context.split('.'); - while(names) { - const checkDef = `${names.join('.')}.${identifier}`; - if(this.definitions.has(checkDef)) { - return this.definitions.get(checkDef)[0]; - } - names.pop(); - } - } -} - - -export interface ScopeTableItem { - //namespace: string - docstring: string - reference: ScopeItem - returnsAs: any -} - -interface ScopeItem { - args: string[] - kwargs: Map -} - -/** - * - * global - * module - * method - * language - * application - */ - - - /** - * When passed an identifier, need to find it. - * There are a number of top level namespaces and - * each may be the object itself. - * - myProject.myModule.mySub.foo - myProject.myModule.foo - myProject.foo - myProject.foo - Application.foo - Vba.foo - - Could be passed any part of the chain. - e.g., myModule.a - - Therefore the resolver needs to first understand the context - of the highest level calling object. - - It also needs to understand the context from where it's being called. - e.g., finding `foo` from myFunc scope, the resolver must recursively - walk up the scope tree to find a definition for foo: - - myProject.myModule.myFunc - - myProject.myModule - - myProject.otherModules (public definitions only) - - myProject - - app - - lang - */ \ No newline at end of file diff --git a/server/src/parser/document/symbols.ts b/server/src/parser/document/symbols.ts deleted file mode 100644 index 653d1c5..0000000 --- a/server/src/parser/document/symbols.ts +++ /dev/null @@ -1,22 +0,0 @@ - -/** - * A data class representing a method, function, or variable. - */ -class VbaSymbol { - scope: string; - name: string; - - constructor(scope: string, name: string, ) { - this.scope = scope; - this.name = name; - } -} - -class VbaReference extends VbaSymbol { - declaration: VbaSymbol; - - constructor(scope: string, name: string, declaration: VbaSymbol) { - super(scope, name); - this.declaration = declaration; - } -} \ No newline at end of file diff --git a/server/src/parser/elements/base.ts b/server/src/parser/elements/base.ts deleted file mode 100644 index 9618d54..0000000 --- a/server/src/parser/elements/base.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { ParserRuleContext } from 'antlr4ts'; -import { Diagnostic, FoldingRange, Hover, Location, MarkupContent, MarkupKind, Range, SemanticTokenModifiers, SemanticTokenTypes, SymbolInformation, SymbolKind } from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { AmbiguousIdentifierContext, LiteralContext } from '../../antlr/out/vbaParser'; -import { SemanticToken } from '../../capabilities/vbaSemanticTokens'; -import { getCtxRange } from '../../utils/helpers'; - - -export interface SyntaxElement { - uri: string; - text: string; - range: Range; - identifier?: IdentifierElement; - context: ParserRuleContext; - symbolKind: SymbolKind; - semanticToken?: SemanticToken; - diagnostics: Diagnostic[]; - name: string; - - readonly namespace: string; - - parent?: SyntaxElement; - children: SyntaxElement[]; - - getAncestorCount(): number; - hover(): Hover | undefined; - isChildOf(element: SyntaxElement): boolean; - symbolInformation(uri: string): SymbolInformation | undefined; - foldingRange(): FoldingRange | undefined; - location(): Location; - addDiagnostics(diagnostics: Diagnostic[]): void; -} - -export interface Identifiable { - ambiguousIdentifier(): AmbiguousIdentifierContext; - ambiguousIdentifier(i: number): AmbiguousIdentifierContext; -} - -export abstract class BaseElement implements SyntaxElement { - abstract name: string; - uri: string; - text: string; - range: Range; - identifier?: IdentifierElement; - context: ParserRuleContext; - symbolKind: SymbolKind; - semanticToken?: SemanticToken; - diagnostics: Diagnostic[] = []; - - parent?: SyntaxElement; - - children: SyntaxElement[] = []; - private _countAncestors = 0; - - protected _hoverContent: MarkupContent | undefined; - - get namespace(): string { - return `${this.parent?.namespace}.${this.identifier?.text}`; - } - - get hoverContent(): MarkupContent { - if(this._hoverContent === undefined) { - this.hoverContent = { - kind: MarkupKind.PlainText, - value: '' - }; - } - return this._hoverContent!; - } - - set hoverContent(value: MarkupContent) { - this._hoverContent = value; - } - - constructor(ctx: ParserRuleContext, doc: TextDocument) { - this.uri = doc.uri; - this.context = ctx; - this.range = getCtxRange(ctx, doc); - this.text = ctx.text; - this.setIdentifierFromDoc(doc); - this.symbolKind = SymbolKind.Null; - } - - hover = () => this.parent?.hover(); - - location = (): Location => Location.create(this.uri, this.range); - - isChildOf(element: SyntaxElement): boolean { - const tr = this.range; - const pr = element.range; - - const psl = pr.start.line; - const psc = pr.start.character; - const tsl = tr.start.line; - const tsc = tr.start.character; - - const pel = pr.end.line; - const pec = pr.end.character; - const tel = tr.end.line; - const tec = tr.end.character; - - const prStartEarlier = (psl < tsl) || (psl === tsl && psc <= tsc); - const prEndsAfter = (pel > tel) || (pel === tel && pec >= tec); - - return prStartEarlier && prEndsAfter; - } - - symbolInformation = (uri: string): SymbolInformation | undefined => - SymbolInformation.create( - this.identifier!.text, - this.symbolKind, - this.range, - uri, - this.parent?.identifier?.text ?? ''); - - foldingRange = (): FoldingRange | undefined => - FoldingRange.create( - this.range.start.line, - this.range.end.line, - this.range.start.character, - this.range.end.character - ); - - addDiagnostics(diagnostics: Diagnostic[]) { - this.diagnostics = this.diagnostics.concat(diagnostics); - } - - getAncestorCount(n = 0): number { - if (this._countAncestors === 0) { - const pnt = this.getParent(); - if (pnt) { - this._countAncestors = pnt.getAncestorCount(n + 1); - return this._countAncestors; - } - } - return this._countAncestors + n; - } - - toString = () => `${"-".repeat(this.getAncestorCount())} ${this.constructor.name}: ${this.context.text}`; - - protected setIdentifierFromDoc(doc: TextDocument): void { - if (this.isIdentifiable(this.context)) { - const identCtx = this.context.ambiguousIdentifier(0); - if (identCtx) { - this.identifier = new IdentifierElement(identCtx, doc); - } - } - } - - protected isIdentifiable = (o: any): o is Identifiable => - 'ambiguousIdentifier' in o; - - private getParent(): BaseElement | undefined { - if (this.parent) { - if (this.parent instanceof BaseElement) { - return this.parent; - } - } - } -} - -export class IdentifierElement extends BaseElement { - name = "IdentifierElement"; - constructor(ctx: AmbiguousIdentifierContext | LiteralContext, doc: TextDocument, name?: string | undefined) { - super(ctx, doc); - if(name) this.name = name; - } - - createSemanticToken(tokType: SemanticTokenTypes, tokMods?: SemanticTokenModifiers[]) { - if (!(this.context instanceof AmbiguousIdentifierContext) && !(this.context instanceof LiteralContext)) - return; - - this.semanticToken = new SemanticToken( - this.range.start.line, - this.range.start.character, - this.text.length, - tokType, - tokMods ?? [] - ); - - - } -} - -export class FoldableElement extends BaseElement { - name = "FoldableElement"; -} - -export class UnknownElement extends BaseElement { - name = "UnknownElement"; -} diff --git a/server/src/parser/elements/memory/base.ts b/server/src/parser/elements/memory/base.ts deleted file mode 100644 index 920551e..0000000 --- a/server/src/parser/elements/memory/base.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { SemanticTokenModifiers, SemanticTokenTypes } from 'vscode-languageserver'; -import { BaseElement } from '../base'; -import { SemanticToken } from '../../../capabilities/vbaSemanticTokens'; -import { VariableDeclarationElement } from './variables'; -import { ArgListContext, ConstStmtContext, VariableStmtContext } from '../../../antlr/out/vbaParser'; -import { TextDocument } from 'vscode-languageserver-textdocument'; - -export enum Visibility { - "Private", - "Friend", - "Public", - "Global" -} - -export enum BaseTypes { - 'string', - 'oct', - 'hex', - 'short', - 'double', - 'integer', - 'date', - 'boolean', - 'variant' -} - -export interface IDeclaration { - asType(): any; -} - -export interface IReference { - declaration(): IDeclaration -} - -export abstract class BaseDeclarations extends BaseElement { - variableList: VariableDeclarationElement[] = []; - - constructor(ctx: VariableStmtContext | ConstStmtContext | ArgListContext, doc: TextDocument) { - super(ctx, doc); - } - - /** - * Creates a semantic token for this declaration. - * @param tokenModifiers the token modifiers as a list. - */ - protected createSemanticToken(tokenModifiers: SemanticTokenModifiers[]) { - const name = this.identifier!; - this.semanticToken = new SemanticToken( - name.range.start.line, - name.range.start.character, - name.text.length, - SemanticTokenTypes.variable, - tokenModifiers - ); - } -} - -// Need to figure out how to handle call chains like MyClass.MyProp.Value -export abstract class BaseDeclaration extends BaseElement { - semanticToken?: SemanticToken | undefined; - visibility = Visibility.Private; - isArray = false; - hasScope = false; - references: BaseReference[] = []; - - get isPublic(): boolean { - return [Visibility.Global, Visibility.Public].includes(this.visibility); - } - - get identifierText(): string { - return this.identifier?.context.text ?? "Undefined"; - } -} - -export abstract class BaseReference extends BaseElement { - declarationElement: BaseDeclaration | undefined; - - get identifierText(): string { - return this.identifier?.context.text ?? "Undefined"; - } -} \ No newline at end of file diff --git a/server/src/parser/elements/memory/constants.ts b/server/src/parser/elements/memory/constants.ts deleted file mode 100644 index 17d24a9..0000000 --- a/server/src/parser/elements/memory/constants.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { ConstStmtContext } from '../../../antlr/out/vbaParser'; -import { BaseDeclarations } from './base'; -import { SemanticTokenModifiers } from 'vscode-languageserver'; - - - -export class ConstDeclarationElement extends BaseDeclarations { - name = "ConstDeclarationElement"; - constructor(ctx: ConstStmtContext, doc: TextDocument) { - super(ctx, doc); - - const modifiers = this.getSemanticTokenModifiers(); - this.createSemanticToken(modifiers); - } - - private getSemanticTokenModifiers = (): SemanticTokenModifiers[] => - [SemanticTokenModifiers.declaration, SemanticTokenModifiers.readonly]; -} \ No newline at end of file diff --git a/server/src/parser/elements/memory/events.ts b/server/src/parser/elements/memory/events.ts deleted file mode 100644 index 9279316..0000000 --- a/server/src/parser/elements/memory/events.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BaseDeclaration, Visibility } from './base'; - - - -export class EventDeclarationElement extends BaseDeclaration { - name = "EventDeclarationElement"; - visibility = Visibility.Public; - // Public Event OnNewLine(pos As Long) -} - -export class EventMethodDeclarationElement extends BaseDeclaration { - name = "EventMethodDeclarationElement"; - visibility = Visibility.Public; - // Acts as both a reference and a declaration. - // Should reference the WithEvents variable. - - // Private WithEvents myRaiser As EventRaiser - // Private Sub myRaiser_OnNewLine(pos As Long) -} \ No newline at end of file diff --git a/server/src/parser/elements/memory/functions.ts b/server/src/parser/elements/memory/functions.ts deleted file mode 100644 index 80e0b38..0000000 --- a/server/src/parser/elements/memory/functions.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { ArgListContext } from '../../../antlr/out/vbaParser'; -import { BaseDeclarations, BaseDeclaration, Visibility } from './base'; - - - -export class FunctionDeclarationElement extends BaseDeclaration { - name = "FunctionDeclarationElement"; - visibility = Visibility.Public; -} - -export class ArgDeclarationsElement extends BaseDeclarations { - name = "ArgDeclarationsElement"; - - constructor(ctx: ArgListContext, doc: TextDocument) { - super(ctx, doc); - } -} \ No newline at end of file diff --git a/server/src/parser/elements/memory/references.ts b/server/src/parser/elements/memory/references.ts deleted file mode 100644 index 688c6e2..0000000 --- a/server/src/parser/elements/memory/references.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BaseDeclaration, BaseReference } from './base'; - - -export class MethodCallReferenceElement extends BaseReference { - name = "MethodCallReferenceElement"; -} - - -export class VariableReferenceElement extends BaseReference { - name = "VariableReferenceElement"; -} - - -export class LiteralReferenceElement extends BaseReference { - name = "LiteralReferenceElement"; -} - -class LiteralDeclarationMockElement extends BaseDeclaration { - name = "LiteralDeclarationMockElement"; -} \ No newline at end of file diff --git a/server/src/parser/elements/memory/types.ts b/server/src/parser/elements/memory/types.ts deleted file mode 100644 index b13978f..0000000 --- a/server/src/parser/elements/memory/types.ts +++ /dev/null @@ -1,23 +0,0 @@ - - -export class Primative { - -} - -export class Struct { - name: string; - type: Struct | Primative; - - constructor(name: string, type: Struct | Primative) { - this.name = name; - this.type = type; - } -} - -export class TypeRegister { - registeredTypes: Map; - - constructor() { - this.registeredTypes = new Map(); - } -} \ No newline at end of file diff --git a/server/src/parser/elements/memory/variables.ts b/server/src/parser/elements/memory/variables.ts deleted file mode 100644 index 7165772..0000000 --- a/server/src/parser/elements/memory/variables.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { VariableStmtContext } from '../../../antlr/out/vbaParser'; -import { BaseDeclaration, BaseDeclarations } from './base'; -import { SemanticTokenModifiers } from 'vscode-languageserver'; - - -export class VariableDeclarationsElement extends BaseDeclarations { - name = "VariableDeclarationsElement"; - - constructor(ctx: VariableStmtContext, doc: TextDocument) { - super(ctx, doc); - - const modifiers = this.getSemanticTokenModifiers(!!(ctx.STATIC())); - this.createSemanticToken(modifiers); - } - - private getSemanticTokenModifiers(isStatic: boolean): SemanticTokenModifiers[] { - const result: SemanticTokenModifiers[] = [SemanticTokenModifiers.declaration]; - if(isStatic) { result.push(SemanticTokenModifiers.static); } - return result; - } -} - -export class VariableDeclarationElement extends BaseDeclaration { - name = "VariableDeclarationElement"; - constructor(ctx: VariableStmtContext, doc: TextDocument) { - super(ctx, doc); - } - - -} \ No newline at end of file diff --git a/server/src/parser/elements/method.ts b/server/src/parser/elements/method.ts deleted file mode 100644 index e102303..0000000 --- a/server/src/parser/elements/method.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { TypeContext } from './vbLang'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { BaseElement, IdentifierElement } from './base'; -import { Hover, MarkupContent, MarkupKind, SymbolKind } from 'vscode-languageserver'; -import { AsTypeClauseContext, DocstringStmtContext, MethodSignatureStmtContext, MethodStmtContext, PropertySignatureStmtContext, PropertyStmtContext } from '../../antlr/out/vbaParser'; - - -export class MethodElement extends BaseElement { - readonly name = "MethodElement"; - returnType: TypeContext | undefined; - hasPrivateModifier = false; - - public get signature(): MethodSignatureStmtContext | PropertySignatureStmtContext | undefined { - if (this.context instanceof MethodStmtContext) - return this.context.methodSignatureStmt(); - if (this.context instanceof PropertyStmtContext) { return this.context.propertySignatureStmt(); } - } - - constructor(ctx: MethodStmtContext | PropertyStmtContext, doc: TextDocument) { - super(ctx, doc); - this.returnType = this.getReturnType(doc); - if (ctx instanceof MethodStmtContext) { - this.setUpMethod(ctx); - } else { - this.setUpProperty(ctx); - } - } - - private setUpMethod(ctx: MethodStmtContext) { - this.hasPrivateModifier = !!(ctx.methodSignatureStmt().visibility()?.PRIVATE); - this.symbolKind = ctx.methodSignatureStmt().FUNCTION() ? - SymbolKind.Function : SymbolKind.Method; - } - - private setUpProperty(ctx: PropertyStmtContext) { - this.hasPrivateModifier = !!(ctx.propertySignatureStmt().visibility()?.PRIVATE); - this.symbolKind = SymbolKind.Property; - } - - private getReturnType(doc: TextDocument): TypeContext | undefined { - const asTypeCtx = this.getTypeContext(); - - if (asTypeCtx) { - const t = asTypeCtx.type_(); - const typeCtx = t.baseType() ?? t.complexType(); - if (typeCtx) { - return new TypeContext(typeCtx, doc); - } - } - } - - private getTypeContext(): AsTypeClauseContext | undefined { - return this.getSignatureContext().asTypeClause(); - } - - protected setIdentifierFromDoc(doc: TextDocument): void { - if (this.isIdentifiable(this.getSignatureContext())) { - const identCtx = this.getSignatureContext().ambiguousIdentifier(); - if (identCtx) { - this.identifier = new IdentifierElement(identCtx, doc); - } - } - } - - private getSignatureContext(): MethodSignatureStmtContext | PropertySignatureStmtContext { - if (this.context instanceof MethodStmtContext) { - return this.context.methodSignatureStmt(); - } - if (this.context instanceof PropertyStmtContext) { - return this.context.propertySignatureStmt(); - } - throw new Error("Method has no signature."); - } - - hover = (): Hover => { - return { - contents: this.getHoverText(), - range: this.range - }; - }; - - getHoverText(): MarkupContent { - const lineBreak = '---\n'; - const signature = this.signature; - const docs = this.getDocstringContent(); - - const result = [ - this.parent?.namespace ?? this.namespace, - `\`\`\`vba\n${signature?.text}\n\`\`\`\n`, - lineBreak, - docs - ]; - - return { - kind: MarkupKind.Markdown, - value: result.join('\n') - }; - } - - private getDocstringContent(): string | undefined { - const docstring = this.children.find(x => x.name == "DocstringElement"); - if (docstring instanceof DocstringElement) - return docstring.toMarkupContentValue(); - } - - -} - -export class DocstringElement extends BaseElement { - readonly name = "DocstringElement"; - constructor(ctx: DocstringStmtContext, doc: TextDocument) { - super(ctx, doc); - } - - toMarkupContentValue(): string { - let lines = this.text.split('\n'); - lines = lines.map(str => str.replace(/^[']\s{0,3}/, '')); - lines = lines.map(str => str.replace(/\s/, ' ')); - return lines.join(' \n'); - } -} diff --git a/server/src/parser/elements/module.ts b/server/src/parser/elements/module.ts deleted file mode 100644 index 6674389..0000000 --- a/server/src/parser/elements/module.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { BaseElement, IdentifierElement } from './base'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { AttributeStmtContext, ModuleContext } from '../../antlr/out/vbaParser'; -import { SymbolKind } from 'vscode-languageserver'; -import { stripQuotes } from '../../utils/helpers'; - -export class ModuleElement extends BaseElement { - readonly name = "ModuleElement"; - - /** - * Override the base namespace function so that it's not recurring. - * Removes the quote marks from the identifier. - */ - get namespace() { - return this.identifier?.name.replace(/^"(.*)"$/, '$1') ?? this.name; - } - - constructor(ctx: ModuleContext, doc: TextDocument) { - super(ctx, doc); - this.symbolKind = SymbolKind.Module; - } - - protected setIdentifierFromDoc(doc: TextDocument): void { - const ctx = this.context as ModuleContext; - const attrs = ctx.moduleAttributes(); - const vbName = attrs?.attributeStmt().find( - x => x.implicitCallStmt_InStmt().text == "VB_Name") - ?.implicitCallStmt_InStmt().iCS_S_VariableOrProcedureCall(); - - if (this.isIdentifiable(vbName)) { - const identCtx = vbName.ambiguousIdentifier(); - if (identCtx) { - this.identifier = new IdentifierElement(identCtx, doc); - } - } - } -} - -export class ModuleAttribute { - identifier?: IdentifierElement; - literal?: IdentifierElement; - - key = (): string => this.identifier?.text ?? 'Undefined'; - value = (): string => (this.literal) ? stripQuotes(this.literal.text) : 'Undefined'; - - constructor(ctx: AttributeStmtContext, doc: TextDocument) { - const idCtx = ctx - .implicitCallStmt_InStmt() - ?.iCS_S_VariableOrProcedureCall() - ?.ambiguousIdentifier(); - - if (idCtx) { - this.identifier = new IdentifierElement(idCtx, doc); - const pntCtx = idCtx.parent?.parent?.parent; - if(pntCtx instanceof AttributeStmtContext) { - this.literal = new IdentifierElement(idCtx, doc, pntCtx.literal(0)?.STRINGLITERAL()?.text); - } - } - } -} \ No newline at end of file diff --git a/server/src/parser/elements/variable.ts b/server/src/parser/elements/variable.ts deleted file mode 100644 index 9cbc890..0000000 --- a/server/src/parser/elements/variable.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { SemanticToken } from '../../capabilities/vbaSemanticTokens'; -import { vbaTypeToSymbolConverter } from '../../utils/converters'; -import { BaseElement, IdentifierElement, SyntaxElement } from './base'; -import { ImplicitCallElement, TypeContext } from './vbLang'; -import { Hover, SemanticTokenModifiers, SemanticTokenTypes, SymbolKind } from 'vscode-languageserver'; - -import { ConstStmtContext, ConstSubStmtContext, ImplicitCallStmt_InBlockContext, ImplicitCallStmt_InStmtContext, LetStmtContext, LiteralContext, SetStmtContext, ValueStmtContext, VariableStmtContext, VariableSubStmtContext, VsLiteralContext } from '../../antlr/out/vbaParser'; -import { MethodElement } from './method'; - -interface IsVarOrCall { - setDeclaredType(e: MethodElement): void; -} - -export enum Type { - 'string' = 0, - 'oct', - 'hex', - 'short', - 'double', - 'integer', - 'date', - 'boolean', - 'object', - 'variant' -} - -/** - * Represents a variable declaration either explicit or via a method signature. - */ -export class VariableStatementElement extends BaseElement implements IsVarOrCall { - readonly name = "VariableStatementElement"; - variableList: VariableDeclarationElement[] = []; - hasPrivateModifier = false; - foldingRange = () => undefined; - declaration: MethodElement | undefined; - - constructor(ctx: VariableStmtContext | ConstStmtContext, doc: TextDocument) { - super(ctx, doc); - this.symbolKind = SymbolKind.Variable; - // TODO: No private modifier is, by default, private. - // MS recommends explicitly using Public or Private rather than Dim at the module level. - // Similar, Global means Public and common convention is not to use Global. - this.hasPrivateModifier = !!(ctx.visibility()?.PRIVATE()); - this.resolveDeclarations(ctx, doc); - } - - hover = (): Hover => this.declaration?.hover() ?? - ({ - contents: this.hoverContent, - range: this.range - }); - - getHoverText(): string { - return "Variant"; - } - - setDeclaredType(e: MethodElement) { - this.declaration = e; - } - - private resolveDeclarations(ctx: VariableStmtContext | ConstStmtContext, doc: TextDocument) { - if (ctx instanceof VariableStmtContext) { - this.resolveVarDeclarations(ctx, doc); - } else { - this.resolveConstDeclarations(ctx, doc); - } - } - - private resolveVarDeclarations(ctx: VariableStmtContext, doc: TextDocument) { - const tokenMods = this.getSemanticTokenModifiers(ctx); - ctx.variableListStmt().variableSubStmt().forEach((x) => - this.variableList.push(new VariableDeclarationElement(x, doc, tokenMods, this.hasPrivateModifier))); - } - - private resolveConstDeclarations(ctx: ConstStmtContext, doc: TextDocument) { - const tokenMods = this.getSemanticTokenModifiers(ctx); - ctx.constSubStmt().forEach((x) => this.variableList.push( - new VariableDeclarationElement(x, doc, tokenMods, this.hasPrivateModifier))); - } - - private getSemanticTokenModifiers(ctx: VariableStmtContext | ConstStmtContext): SemanticTokenModifiers[] { - const result: SemanticTokenModifiers[] = [SemanticTokenModifiers.declaration]; - if (ctx instanceof VariableStmtContext && ctx.STATIC()) - result.push(SemanticTokenModifiers.static); - if (ctx instanceof ConstStmtContext) - result.push(SemanticTokenModifiers.readonly); - return result; - } -} - -export class VariableDeclarationElement extends BaseElement { - readonly name = "VariableDeclarationElement"; - asType: TypeContext | undefined; - hasPrivateModifier = false; - - constructor(ctx: VariableSubStmtContext | ConstSubStmtContext, doc: TextDocument, tokenModifiers: SemanticTokenModifiers[], hasPrivateModifier: boolean) { - super(ctx, doc); - this.hasPrivateModifier = hasPrivateModifier; - this.asType = TypeContext.create(ctx, doc); - this.symbolKind = vbaTypeToSymbolConverter(this.asType?.text ?? 'variant'); - this.createSemanticToken(tokenModifiers); - } - - private createSemanticToken(tokenModifiers: SemanticTokenModifiers[]) { - const name = this.identifier!; - this.semanticToken = new SemanticToken( - name.range.start.line, - name.range.start.character, - name.text.length, - SemanticTokenTypes.variable, - tokenModifiers - ); - } -} - -export class VariableAssignElement extends BaseElement { - readonly name = "VariableAssignElement"; - - private _leftImplicitCall: ImplicitCallElement | undefined; - private _rightImplicitCall: ImplicitCallElement | undefined; - - get leftImplicitCall() { - return this._leftImplicitCall!; - } - get rightImplicitCall() { - return this._rightImplicitCall!; - } - - constructor(ctx: LetStmtContext | SetStmtContext, doc: TextDocument) { - super(ctx, doc); - const callStmtContext = ctx.implicitCallStmt_InStmt(); - const varOrProc = callStmtContext.iCS_S_VariableOrProcedureCall(); - if (varOrProc) { - this.identifier = new IdentifierElement(varOrProc.ambiguousIdentifier(), doc); - return; - } - } - symbolInformation = (_: string) => undefined; - - /** - * Adds an implicit call to the left or right side of the assignment. - * @param implicitCall the implicit call to add. - */ - addImplicitCall(implicitCall: ImplicitCallElement) { - if (this._leftImplicitCall) { - this._rightImplicitCall = implicitCall; - this.validateTypes(); - } - else { - this._leftImplicitCall = implicitCall; - } - } - - private validateTypes() { - return false; - } -} - -/** - * Represents a literal, variable, or call that can be assigned to a variable. - */ -export class AssignmentElement extends BaseElement { - readonly name = "AssignmentElement"; - variableType: Type; - - constructor(ctx: ValueStmtContext | ImplicitCallStmt_InStmtContext, doc: TextDocument) { - super(ctx, doc); - this.variableType = this.getType(ctx); - } - - private getType(ctx: ValueStmtContext | ImplicitCallStmt_InStmtContext): Type { - if(ctx instanceof VsLiteralContext) { - const litCtx = ctx.literal(); - - if(litCtx.STRINGLITERAL()) { - return Type.string; - } - - if(litCtx.INTEGERLITERAL()) { - return Type.integer; - } - - if(litCtx.DOUBLELITERAL()) { - return Type.double; - } - - if(litCtx.TRUE() || litCtx.FALSE()) { - return Type.boolean; - } - - if(litCtx.DATELITERAL()) { - return Type.date; - } - - if(litCtx.SHORTLITERAL()) { - return Type.short; - } - - if(litCtx.NOTHING() || litCtx.NULL_()) { - return Type.variant; - } - - - if(litCtx.HEXLITERAL()) { - return Type.hex; - } - - if(litCtx.OCTLITERAL()) { - return Type.oct; - } - - throw new Error("Unknown literal type."); - - } - else { - - return Type.variant; - } - } - - private getLiteralType(ctx: LiteralContext): Type { - - return Type.variant; - } - - -} \ No newline at end of file diff --git a/server/src/parser/elements/vbLang.ts b/server/src/parser/elements/vbLang.ts deleted file mode 100644 index 8cb76c2..0000000 --- a/server/src/parser/elements/vbLang.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { BaseElement } from './base'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { BaseTypeContext, ComplexTypeContext, ConstSubStmtContext, VariableSubStmtContext } from '../../antlr/out/vbaParser'; - -export class TypeContext extends BaseElement { - readonly name = "TypeContext"; - constructor(ctx: BaseTypeContext | ComplexTypeContext, doc: TextDocument) { - super(ctx, doc); - } - - static create(ctx: VariableSubStmtContext | ConstSubStmtContext, doc: TextDocument): TypeContext | undefined { - const xCtx = ctx.asTypeClause()?.type_(); - const tCtx = xCtx?.baseType() ?? xCtx?.complexType(); - if (tCtx) { return new TypeContext(tCtx, doc); } - } -} - - -import { SemanticTokenTypes, SymbolKind } from 'vscode-languageserver'; -import { EnumerationStmtContext, EnumerationStmt_ConstantContext } from '../../antlr/out/vbaParser'; - -export class EnumElement extends BaseElement { - readonly name = "EnumElement"; - constructor(ctx: EnumerationStmtContext, doc: TextDocument) { - super(ctx, doc); - this.symbolKind = SymbolKind.Enum; - if (this.identifier) - this.identifier.createSemanticToken(SemanticTokenTypes.enum); - } -} - -export class EnumConstantElement extends BaseElement { - readonly name = "EnumConstant"; - foldingRange = () => undefined; - symbolInformation = (_: string) => undefined; - constructor(ctx: EnumerationStmt_ConstantContext, doc: TextDocument) { - super(ctx, doc); - this.symbolKind = SymbolKind.EnumMember; - if (this.identifier) { - this.identifier.createSemanticToken( - SemanticTokenTypes.enumMember); - } - } -} - - -import { LiteralContext } from '../../antlr/out/vbaParser'; - -export class LiteralElement extends BaseElement { - name = "LiteralElement"; - private _valueString: string; - - constructor(ctx: LiteralContext, doc: TextDocument, name?: string) { - super(ctx, doc); - this._valueString = ctx.STRINGLITERAL.toString(); - if(name) this.name = name; - } - - get valueString() { - return this._valueString; - } -} - - -import { ImplicitCallStmt_InBlockContext, ImplicitCallStmt_InStmtContext } from '../../antlr/out/vbaParser'; - -/** -* An implicit call can refer to a member call -* with a variable or procedure, e.g., -* Module1.Greeting, -* dict.Item(itemKey). -*/ -export class ImplicitCallElement extends BaseElement { - readonly name = "ImplicitCallElement"; - constructor(ctx: ImplicitCallStmt_InBlockContext | ImplicitCallStmt_InStmtContext, doc: TextDocument) { - super(ctx, doc); - - } -} \ No newline at end of file diff --git a/server/src/parser/vbaSyntaxParser.ts b/server/src/parser/vbaSyntaxParser.ts deleted file mode 100644 index 488ffa8..0000000 --- a/server/src/parser/vbaSyntaxParser.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { ANTLRInputStream, CommonTokenStream, ConsoleErrorListener, ParserRuleContext, RecognitionException, Recognizer } from 'antlr4ts'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { vbaListener } from '../antlr/out/vbaListener'; -import { AttributeStmtContext, ConstStmtContext, DocstringStmtContext, EnumerationStmtContext, EnumerationStmt_ConstantContext, FoldingBlockStmtContext, MethodStmtContext, IfThenElseStmtContext, ImplicitCallStmt_InBlockContext, ImplicitCallStmt_InStmtContext, LetStmtContext, ModuleContext, ModuleHeaderContext, SetStmtContext, StartRuleContext, UnknownLineContext, VariableStmtContext, vbaParser, PropertyStmtContext, ICS_S_VariableOrProcedureCallContext, ICS_S_MembersCallContext, AmbiguousIdentifierContext, ModuleConfigContext, ArgListContext } from '../antlr/out/vbaParser'; -import { vbaLexer as VbaLexer } from '../antlr/out/vbaLexer'; -import { ParseTreeWalker } from 'antlr4ts/tree/ParseTreeWalker'; -import { ErrorNode } from 'antlr4ts/tree/ErrorNode'; -import { DocstringElement, MethodElement } from './elements/method'; -import { SymbolKind } from 'vscode-languageserver'; -import { ModuleAttribute, ModuleElement } from './elements/module'; -import { FoldableElement, SyntaxElement } from './elements/base'; -import { VariableAssignElement, VariableDeclarationElement, VariableStatementElement } from './elements/variable'; -import { EnumConstantElement, EnumElement } from './elements/vbLang'; - - -export interface ResultsContainer { - module?: ModuleElement; - addModule(element: ModuleElement): void; - addElement(element: SyntaxElement): ResultsContainer; - addScopeReferences(emt: VariableAssignElement): void; - addScopeDeclaration(emt: MethodElement | VariableDeclarationElement): void; - addFoldingRange(emt: FoldableElement): void; -} - -// TODO: Break down into smaller blocks of work. -// The parser is attempting to parse all of VBA. It would be more efficient to parse each function separately. -// A pre-parser should detect module level elements, and if anything has changed, the element should be re-parsed -// using the lower parser, e.g., funcitons / subs / props / decs, etc. The part of the document that has changed -// should also force a refresh to that specific element. -export class SyntaxParser { - parse(doc: TextDocument, resultsContainer: ResultsContainer) { - const listener = new VbaTreeWalkListener(doc, resultsContainer); - const parser = this.createParser(doc); - - ParseTreeWalker.DEFAULT.walk( - listener, - parser.startRule() - ); - } - - private createParser(doc: TextDocument): vbaParser { - const lexer = new VbaLexer(new ANTLRInputStream(doc.getText())); - const parser = new vbaParser(new CommonTokenStream(lexer)); - - parser.removeErrorListeners(); - parser.addErrorListener(new VbaErrorListener()); - return parser; - } -} - -class VbaTreeWalkListener implements vbaListener { - doc: TextDocument; - resultsContainer: ResultsContainer; - - // State flags - inModuleConfig = false; - inAttributeStatement = false; - inDeclarationStatement = false; - - constructor(doc: TextDocument, resultsContainer: ResultsContainer) { - this.doc = doc; - this.resultsContainer = resultsContainer; - } - - enterFoldingBlockStmt = (ctx: FoldingBlockStmtContext) => - this.registerFoldingRange(ctx); - - enterIfThenElseStmt?: ((ctx: IfThenElseStmtContext) => void) | undefined; - - visitErrorNode(node: ErrorNode) { - console.log(node.payload); - } - - enterUnknownLine = (ctx: UnknownLineContext) => { - console.log(ctx.text); - }; - - enterModule = (ctx: ModuleContext) => - this.resultsContainer.addModule( - new ModuleElement(ctx, this.doc)); - - // Only classes have a header. TODO: Test this for forms. - enterModuleHeader = (_: ModuleHeaderContext) => - this.resultsContainer.module!.symbolKind = SymbolKind.Class; - - enterAttributeStmt = (ctx: AttributeStmtContext) => { - this.inAttributeStatement = true; - const attr = new ModuleAttribute(ctx, this.doc); - // this.resultsContainer.setModuleAttribute(attr); - if (attr.identifier?.text === 'VB_Name') { - const module = this.resultsContainer.module; - if (module) { - module.identifier = attr.literal; - } - } - }; - - exitAttributeStmt = (_: AttributeStmtContext) => this.inAttributeStatement = false; - - enterModuleConfig = (_: ModuleConfigContext) => this.inModuleConfig = true; - exitModuleConfig = (_: ModuleConfigContext) => this.inModuleConfig = false; - - enterMethodStmt = (ctx: MethodStmtContext) => { - const e = new MethodElement(ctx, this.doc); - this.registerFoldingRange(ctx); - this.resultsContainer - .addElement(e) - .addScopeDeclaration(e); - }; - - enterPropertyStmt = (ctx: PropertyStmtContext) => { - const e = new MethodElement(ctx, this.doc); - this.registerFoldingRange(ctx); - this.resultsContainer - .addElement(e) - .addScopeDeclaration(e); - }; - - enterDocstringStmt = (ctx: DocstringStmtContext) => - this.resultsContainer.addElement( - new DocstringElement(ctx, this.doc) - ); - - enterEnumerationStmt = (ctx: EnumerationStmtContext) => { - this.resultsContainer.addElement( - new EnumElement(ctx, this.doc)); - this.registerFoldingRange(ctx); - }; - - enterEnumerationStmt_Constant = (ctx: EnumerationStmt_ConstantContext) => - this.resultsContainer.addElement( - new EnumConstantElement(ctx, this.doc)); - - enterVariableStmt = (ctx: VariableStmtContext) => this.enterVarOrConstStmt(ctx); - enterConstStmt = (ctx: ConstStmtContext) => this.enterVarOrConstStmt(ctx); - - private enterVarOrConstStmt(ctx: VariableStmtContext | ConstStmtContext) { - const declaration = new VariableStatementElement(ctx, this.doc); - this.resultsContainer.addElement(declaration); - declaration.variableList.forEach((v) => this.resultsContainer.addScopeDeclaration(v)); - } - - enterImplicitCallStmt_InStmt = (ctx: ImplicitCallStmt_InStmtContext) => this.enterImplicitCallStmt(ctx); - enterImplicitCallStmt_InBlock = (ctx: ImplicitCallStmt_InBlockContext) => this.enterImplicitCallStmt(ctx); - - private enterImplicitCallStmt(ctx: ImplicitCallStmt_InStmtContext | ImplicitCallStmt_InBlockContext) { - // console.log('imp call ' + ctx.text); - } - - enterICS_S_MembersCall?: ((ctx: ICS_S_MembersCallContext) => void) | undefined; - enterICS_S_VariableOrProcedureCall?: ((ctx: ICS_S_VariableOrProcedureCallContext) => void) | undefined; - - enterAmbiguousIdentifier = (ctx: AmbiguousIdentifierContext) => { - if (this.inModuleConfig || this.inAttributeStatement) - return; - - // if(ctx.ambiguousKeyword(0).text == "Me") { - // console.log(`me: ${ctx.text}`); - // } - - try { - if (ctx.ambiguousKeyword().length == 0) { - const ctxText = ctx.text; - let pntText = ctxText; - let pnt: ParserRuleContext = ctx; - - while (ctxText == pntText && pnt.parent) { - pnt = pnt.parent; - pntText = pnt.text; - } - - console.log(`ident: ${ctxText}: ${pntText}`); - return; - } - if(ctx.ambiguousKeyword(0).text == "Me") { - console.log(`ident: ${ctx.text}`); - } - } - catch (e: any) { - console.log(`error: ${ctx.toString()}`); - console.log(e); - } - }; - - - enterLetStmt = (ctx: LetStmtContext) => this.enterAssignStmt(ctx); - enterSetStmt = (ctx: SetStmtContext) => this.enterAssignStmt(ctx); - - private enterAssignStmt(ctx: LetStmtContext | SetStmtContext) { - try { - const assignment = new VariableAssignElement(ctx, this.doc); - this.resultsContainer - .addElement(assignment) - .addScopeReferences(assignment); - - // Need to add a variable or call reference element. - // Should also take into account literals. And I don't think - // the logic should be here - add method to resultsContainer - // const left = ctx.implicitCallStmt_InStmt(); - - // const right = ctx.valueStmt(); - // this.resultsContainer.addScopeReference(assignment.leftImplicitCall) - } - catch (e) { - console.log(`ERROR: ${ctx.text}\n${e}`); - } - } - - private registerFoldingRange(ctx: ParserRuleContext) { - this.resultsContainer.addFoldingRange( - new FoldableElement(ctx, this.doc)); - } -} - -class VbaErrorListener extends ConsoleErrorListener { - syntaxError(recognizer: Recognizer, offendingSymbol: T, line: number, charPositionInLine: number, msg: string, e: RecognitionException | undefined): void { - super.syntaxError(recognizer, offendingSymbol, line, charPositionInLine, msg, e); - console.error(e); - if (e) { - const y = recognizer.getErrorHeader(e); - console.log(y); - } - recognizer.inputStream?.consume(); - } -} \ No newline at end of file diff --git a/server/src/project/document.ts b/server/src/project/document.ts new file mode 100644 index 0000000..08b6377 --- /dev/null +++ b/server/src/project/document.ts @@ -0,0 +1,193 @@ +import { Diagnostic, SemanticTokens, SymbolInformation, SymbolKind } from 'vscode-languageserver'; +import { Workspace } from './workspace'; +import { FoldableElement } from './elements/special'; +import { BaseSyntaxElement, HasAttribute, HasSemanticToken, HasSymbolInformation } from './elements/base'; +import { Range, TextDocument } from 'vscode-languageserver-textdocument'; +import { SyntaxParser } from './parser/vbaSyntaxParser'; +import { FoldingRange } from '../capabilities/folding'; +import { SemanticTokensManager } from '../capabilities/semanticTokens'; +import { sleep } from '../utils/helpers'; + + +export interface ProjectDocument { + name: string; + textDocument: TextDocument; + languageServerSemanticTokens: (range?: Range) => SemanticTokens | null; + languageServerSymbolInformationAsync(): Promise; + get foldableElements(): FoldingRange[]; + parse(): void; +} + + +export abstract class BaseProjectDocument implements ProjectDocument { + readonly workspace: Workspace; + readonly textDocument: TextDocument; + readonly name: string; + + protected _unhandledNamedElements: [] = []; + protected _publicScopeDeclarations: Map = new Map(); + protected _documentScopeDeclarations: Map> = new Map(); + + protected _diagnostics: Diagnostic[] = []; + protected _elementParents: BaseSyntaxElement[] = []; + protected _attributeElements: HasAttribute[] = []; + protected _foldableElements: FoldingRange[] = []; + protected _symbolInformations: SymbolInformation[] = []; + protected _semanticTokens: SemanticTokensManager = new SemanticTokensManager(); + + isBusy = false; + abstract symbolKind: SymbolKind + + get foldableElements() { + return this._foldableElements; + } + + get activeAttributeElement() { + return this._attributeElements?.at(-1); + } + + constructor(workspace: Workspace, name: string, document: TextDocument) { + this.textDocument = document; + this.workspace = workspace; + this.name = name; + } + + static create(workspace: Workspace, document: TextDocument): VbaClassDocument | VbaModuleDocument { + const slashParts = document.uri.split('/').at(-1); + const dotParts = slashParts?.split('.'); + const extension = dotParts?.at(-1); + const filename = dotParts?.join('.'); + + if (!filename || !extension) { + throw new Error("Unable to parse document uri"); + } + + switch (extension) { + case 'cls': + return new VbaClassDocument(workspace, filename, document, SymbolKind.Class); + case 'bas': + return new VbaModuleDocument(workspace, filename, document, SymbolKind.Class); + default: + throw new Error("Expected *.cls or *.bas but got *." + extension); + } + } + + languageServerSemanticTokens = (range?: Range) => { + return this._semanticTokens.getSemanticTokens(range); + }; + + async languageServerSymbolInformationAsync() { + while (this.isBusy) { + await sleep(5); + } + return this._symbolInformations; + } + + parse = (): void => { + this.isBusy = true; + console.log('Parsing document'); + (new SyntaxParser()).parse(this); + console.log('Finished parsing document'); + this.isBusy = false; + }; + + registerNamedElementDeclaration(element: any) { + // Check workspace if public. + // Check for existing entry in local scope. + // Check for overriding name in method (if relevant) + // Check for overriding name in document scope. + // Check for overriding name in workspace. + throw new Error("Not implemented"); + } + + /** + * Pushes an element to the attribute elements stack. + * Be careful to pair a register action with an appropriate deregister. + * @param element the element to register. + * @returns nothing of interest. + */ + registerAttributeElement = (element: HasAttribute) => { + this._attributeElements.push(element); + return this; + }; + + /** + * Pops an element from the attribute elements stack. + * Popping allows actions to be performed on the same element, + * e.g., registered in the entry event and deregistered in the exit event. + * @param element the element to register. + * @returns the element at the end of the stack. + */ + deregisterAttributeElement = () => { + return this._attributeElements.pop(); + }; + + registerFoldableElement = (element: FoldableElement) => { + this._foldableElements.push(new FoldingRange(element)); + return this; + }; + + registerNamedElement(element: BaseSyntaxElement) { + return this; + } + + /** + * Registers an element as a parent to be attached to subsequent elemements. + * Should be called when the parser context is entered and matched with + * deregisterScopedElement when the context exits. + * @param element the element to register. + * @returns this for chaining. + */ + registerScopedElement(element: BaseSyntaxElement) { + this._elementParents.push(element); + return this; + } + + /** + * Deregisters an element as a parent so it isn't attached to subsequent elemements. + * Should be called when the parser context is exited and matched with + * deregisterScopedElement when the context is entered. + * @returns this for chaining. + */ + deregisterScopedElement = () => { + this._elementParents.pop(); + return this; + }; + + /** + * Registers a semantic token element for tracking with the SemanticTokenManager. + * @param element element The element that has a semantic token. + * @returns void. + */ + registerSemanticToken = (element: HasSemanticToken): void => { + this._semanticTokens.add(element); + }; + + /** + * Registers a SymbolInformation. + * @param element The element that has symbol information. + * @returns a number for some reason. + */ + registerSymbolInformation = (element: HasSymbolInformation): number => { + console.debug(`Registering symbol for ${element.symbolInformation.name}`); + return this._symbolInformations.push(element.symbolInformation); + }; +} + + +export class VbaClassDocument extends BaseProjectDocument { + symbolKind: SymbolKind; + constructor(workspace: Workspace, name: string, document: TextDocument, symbolKind: SymbolKind) { + super(workspace, name, document); + this.symbolKind = symbolKind; + } +} + + +export class VbaModuleDocument extends BaseProjectDocument { + symbolKind: SymbolKind; + constructor(workspace: Workspace, name: string, document: TextDocument, symbolKind: SymbolKind) { + super(workspace, name, document); + this.symbolKind = symbolKind; + } +} diff --git a/server/src/project/elements/base.ts b/server/src/project/elements/base.ts new file mode 100644 index 0000000..3a3cdd3 --- /dev/null +++ b/server/src/project/elements/base.ts @@ -0,0 +1,105 @@ +import { ParserRuleContext } from 'antlr4ts'; +import { Range, SemanticTokenModifiers, SemanticTokenTypes, SymbolInformation, SymbolKind } from 'vscode-languageserver'; +import { Position, TextDocument } from 'vscode-languageserver-textdocument'; +import { FoldingRangeKind } from '../../capabilities/folding'; +import { IdentifierElement } from './memory'; +import { AttributeStmtContext } from '../../antlr/out/vbaParser'; +import '../../extensions/parserExtensions'; + +export interface ContextOptionalSyntaxElement { + range?: Range; + parent?: ContextOptionalSyntaxElement; + context?: ParserRuleContext; + get text(): string; + get uri(): string; + + isChildOf(range: Range): boolean; +} + +interface SyntaxElement extends ContextOptionalSyntaxElement { + range: Range; + context: ParserRuleContext; +} + +export interface HasAttribute { + processAttribute(context: AttributeStmtContext): void; +} + +export interface NamedSyntaxElement extends SyntaxElement { + get name(): string; +} + +export interface IdentifiableSyntaxElement extends NamedSyntaxElement { + identifier: IdentifierElement; +} + +export interface HasSymbolInformation extends NamedSyntaxElement { + symbolInformation: SymbolInformation; +} + +export interface HasSemanticToken extends NamedSyntaxElement, IdentifiableSyntaxElement { + tokenType: SemanticTokenTypes; + tokenModifiers: SemanticTokenModifiers[]; +} + +export interface MemoryElement extends BaseSyntaxElement { + name: string; + returnType: any; + symbol: SymbolKind; +} + +export interface FoldingRangeElement { + range: Range; + foldingRangeKind?: FoldingRangeKind; +} + +export abstract class BaseSyntaxElement implements ContextOptionalSyntaxElement { + protected document: TextDocument; + + range?: Range; + parent?: ContextOptionalSyntaxElement; + context?: ParserRuleContext; + + get text(): string { return this.context?.text ?? ''; } + get uri(): string { return this.document.uri; } + + constructor(context: ParserRuleContext | undefined, document: TextDocument) { + this.context = context; + this.document = document; + this.range = this._contextToRange(); + } + + isChildOf = (range: Range): boolean => { + if (!this.range) { + return false; + } + + const isPositionBefore = (x: Position, y: Position) => + x.line < y.line || (x.line === y.line && x.character <= y.character); + + return isPositionBefore(range.start, this.range.start) + && isPositionBefore(this.range.end, range.end); + }; + + private _contextToRange(): Range | undefined { + if (!this.context) { + return; + } + + const startIndex = this.context.start.startIndex; + const stopIndex = this.context.stop?.stopIndex ?? startIndex; + return Range.create( + this.document.positionAt(startIndex), + this.document.positionAt(stopIndex) + ); + } +} + +export abstract class BaseContextSyntaxElement extends BaseSyntaxElement { + range!: Range; + context!: ParserRuleContext; + + constructor(ctx: ParserRuleContext, doc: TextDocument) { + super(ctx, doc); + } +} \ No newline at end of file diff --git a/server/src/project/elements/memory.ts b/server/src/project/elements/memory.ts new file mode 100644 index 0000000..2d07aa1 --- /dev/null +++ b/server/src/project/elements/memory.ts @@ -0,0 +1,189 @@ +import { AmbiguousIdentifierContext, AsTypeClauseContext, ConstStmtContext, ConstSubStmtContext, EnumerationStmtContext, EnumerationStmt_ConstantContext, MethodStmtContext, VariableStmtContext, VariableSubStmtContext } from '../../antlr/out/vbaParser'; + +import { TextDocument } from 'vscode-languageserver-textdocument'; + +import { BaseContextSyntaxElement, BaseSyntaxElement, HasSemanticToken, HasSymbolInformation } from './base'; +import { SemanticTokenModifiers, SemanticTokenTypes, SymbolInformation, SymbolKind } from 'vscode-languageserver'; +import { FoldableElement } from './special'; +import { SymbolInformationFactory } from '../../capabilities/symbolInformation'; +import '../../extensions/parserExtensions'; + + +export class IdentifierElement extends BaseContextSyntaxElement { + constructor(ctx: AmbiguousIdentifierContext, doc: TextDocument) { + super(ctx, doc); + } +} + + +abstract class BaseEnumElement extends FoldableElement implements HasSemanticToken, HasSymbolInformation { + identifier: IdentifierElement; + tokenModifiers: SemanticTokenModifiers[] = []; + abstract tokenType: SemanticTokenTypes; + abstract symbolKind: SymbolKind; + + constructor(context: EnumerationStmtContext | EnumerationStmt_ConstantContext, document: TextDocument) { + super(context, document); + this.identifier = new IdentifierElement(context.ambiguousIdentifier(), document); + } + + get name(): string { return this.identifier.text; } + get symbolInformation(): SymbolInformation { + return SymbolInformationFactory.create( + this, this.symbolKind + ); + } + +} + + +export class EnumBlockDeclarationElement extends BaseEnumElement { + tokenType: SemanticTokenTypes; + tokenModifiers: SemanticTokenModifiers[] = []; + symbolKind: SymbolKind; + + constructor(context: EnumerationStmtContext, document: TextDocument) { + super(context, document); + this.tokenType = SemanticTokenTypes.enum; + this.symbolKind = SymbolKind.Enum; + } +} + + +export class EnumMemberDeclarationElement extends BaseEnumElement { + tokenType: SemanticTokenTypes; + tokenModifiers: SemanticTokenModifiers[] = []; + symbolKind: SymbolKind; + + constructor(context: EnumerationStmt_ConstantContext, document: TextDocument) { + super(context, document); + this.tokenType = SemanticTokenTypes.enumMember; + this.symbolKind = SymbolKind.EnumMember; + } +} + +abstract class BaseMethodElement extends FoldableElement implements HasSemanticToken, HasSymbolInformation { + identifier: IdentifierElement; + tokenModifiers: SemanticTokenModifiers[] = []; + abstract tokenType: SemanticTokenTypes; + abstract symbolKind: SymbolKind; + + constructor(context: MethodStmtContext, document: TextDocument) { + super(context, document); + this.identifier = new IdentifierElement(context.methodSignatureStmt().ambiguousIdentifier(), document); + } + + get name(): string { return this.identifier.text; } + get symbolInformation(): SymbolInformation { + return SymbolInformationFactory.create( + this, this.symbolKind + ); + } +} + +export class MethodBlockDeclarationElement extends BaseMethodElement { + tokenType: SemanticTokenTypes; + tokenModifiers: SemanticTokenModifiers[] = []; + symbolKind: SymbolKind; + + constructor(context: MethodStmtContext, document: TextDocument) { + super(context, document); + this.tokenType = SemanticTokenTypes.method; + this.symbolKind = SymbolKind.Method; + } +} + +abstract class BaseVariableDeclarationStatementElement extends BaseContextSyntaxElement { + abstract declarations: VariableDeclarationElement[]; + + constructor(context: ConstStmtContext | VariableStmtContext, document: TextDocument) { + super(context, document); + } +} + +export class ConstDeclarationsElement extends BaseVariableDeclarationStatementElement { + declarations: VariableDeclarationElement[] = []; + + constructor(context: ConstStmtContext, document: TextDocument) { + super(context, document); + context.constSubStmt().forEach((element) => + this.declarations.push(new VariableDeclarationElement( + element, document + )) + ); + } +} + +export class VariableDeclarationsElement extends BaseVariableDeclarationStatementElement { + declarations: VariableDeclarationElement[] = []; + + constructor(context: VariableStmtContext, document: TextDocument) { + super(context, document); + context.variableListStmt().variableSubStmt().forEach((element) => + this.declarations.push(new VariableDeclarationElement( + element, document + )) + ); + } +} + +class VariableDeclarationElement extends BaseContextSyntaxElement implements HasSymbolInformation { + identifier: IdentifierElement; + asType: VariableType; + arrayBounds?: ArrayBounds; + + constructor(context: ConstSubStmtContext | VariableSubStmtContext, document: TextDocument) { + super(context, document); + this.asType = new VariableType(context.asTypeClause(), document); + this.arrayBounds = ArrayBounds.create(context); + this.identifier = new IdentifierElement(context.ambiguousIdentifier(), document); + } + + get name(): string { return this.identifier.text; } + get symbolInformation(): SymbolInformation { + return SymbolInformationFactory.create( + this, this.asType.symbolKind + ); + } +} + +class VariableType extends BaseSyntaxElement { + typeName: string; + symbolKind: SymbolKind; + + constructor(context: AsTypeClauseContext | undefined, document: TextDocument, isArray?: boolean) { + super(context, document); + this.symbolKind = isArray ? SymbolKind.Array : SymbolKind.Variable; + + // Needs more ternery. + const type = context?.type_()?.baseType() ?? context?.type_()?.complexType(); + this.typeName = type?.text ?? type?.text ?? 'Variant'; + this.symbolKind = type ? type.toSymbolKind() : SymbolKind.Variable; + } +} + +class ArrayBounds { + dimensions: { lower: number, upper: number }[] = []; + + constructor(subStmt: VariableSubStmtContext) { + subStmt.subscripts()?.subscript_().forEach((x) => { + const vals = x.valueStmt(); + this.dimensions.push({ + lower: vals.length === 1 ? 0 : +vals[0].text, + upper: vals.length === 1 ? +vals[0].text : +vals[1].text + }); + }); + } + + /** + * Creates an ArrayBounds if the context is a variable and an array. + * @param subStmt a subStmt context for a variable or a constant. + * @returns A new array bounds if the context is an array variable. + */ + static create(subStmt: VariableSubStmtContext | ConstSubStmtContext) { + const hasLparenMethod = (x: any): x is VariableSubStmtContext => 'LPAREN' in x; + if (hasLparenMethod(subStmt) && subStmt.LPAREN()) { + return new ArrayBounds(subStmt); + } + } +} \ No newline at end of file diff --git a/server/src/project/elements/module.ts b/server/src/project/elements/module.ts new file mode 100644 index 0000000..4b97af0 --- /dev/null +++ b/server/src/project/elements/module.ts @@ -0,0 +1,45 @@ +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { SymbolInformation, SymbolKind } from 'vscode-languageserver'; +import { AttributeStmtContext, ModuleContext } from '../../antlr/out/vbaParser'; + +import { BaseContextSyntaxElement, HasAttribute, HasSymbolInformation } from './base'; +import { SymbolInformationFactory } from '../../capabilities/symbolInformation'; + + +export class ModuleElement extends BaseContextSyntaxElement implements HasSymbolInformation, HasAttribute { + private _hasName = false; + private _name: string; + symbolKind: SymbolKind; + + constructor(context: ModuleContext, document: TextDocument, symbolKind: SymbolKind) { + super(context, document); + this._name = "Unknown Module"; + this.symbolKind = symbolKind; + } + + get name(): string { + return this._name; + } + + get symbolInformation(): SymbolInformation { + console.warn(`Creating symbol with name ${this._name}`); + return SymbolInformationFactory.create( + this, this.symbolKind + ); + } + + processAttribute(context: AttributeStmtContext): void { + if (this._hasName) { + return; + } + + const text = context.text; + if (text.startsWith("Attribute VB_Name = ")) { + const unquote = (x: string): string => + x.replace(/^"+|"+$/g, ''); + + this._name = unquote(text.split("= ")[1]); + this._hasName = true; + } + } +} diff --git a/server/src/project/elements/special.ts b/server/src/project/elements/special.ts new file mode 100644 index 0000000..876cd8c --- /dev/null +++ b/server/src/project/elements/special.ts @@ -0,0 +1,15 @@ +import { ParserRuleContext } from 'antlr4ts'; +import { FoldingRangeKind } from '../../capabilities/folding'; +import { BaseContextSyntaxElement, FoldingRangeElement } from './base'; +import { Range, TextDocument } from 'vscode-languageserver-textdocument'; + + +export class FoldableElement extends BaseContextSyntaxElement implements FoldingRangeElement { + range!: Range; + foldingRangeKind?: FoldingRangeKind; + + constructor(ctx: ParserRuleContext, doc: TextDocument, foldingRangeKind?: FoldingRangeKind) { + super(ctx, doc); + this.foldingRangeKind = foldingRangeKind; + } +} diff --git a/server/src/project/parser/vbaSyntaxParser.ts b/server/src/project/parser/vbaSyntaxParser.ts new file mode 100644 index 0000000..c28c466 --- /dev/null +++ b/server/src/project/parser/vbaSyntaxParser.ts @@ -0,0 +1,128 @@ +import { TextDocument } from 'vscode-languageserver-textdocument'; + +import { ANTLRInputStream, CommonTokenStream, ConsoleErrorListener, RecognitionException, Recognizer } from 'antlr4ts'; + +import { ErrorNode } from 'antlr4ts/tree/ErrorNode'; +import { ParseTreeWalker } from 'antlr4ts/tree/ParseTreeWalker'; + +import { vbaLexer as VbaLexer } from '../../antlr/out/vbaLexer'; +import { AttributeStmtContext, ConstStmtContext, EnumerationStmtContext, EnumerationStmt_ConstantContext, FoldingBlockStmtContext, MethodStmtContext, ModuleContext, ModuleHeaderContext, VariableStmtContext, vbaParser as VbaParser } from '../../antlr/out/vbaParser'; +import { vbaListener } from '../../antlr/out/vbaListener'; + +import { VbaClassDocument, VbaModuleDocument } from '../document'; +import { FoldableElement } from '../elements/special'; +import { ConstDeclarationsElement, EnumBlockDeclarationElement, EnumMemberDeclarationElement, MethodBlockDeclarationElement, VariableDeclarationsElement } from '../elements/memory'; +import { ModuleElement } from '../elements/module'; + +export class SyntaxParser { + parse(document: VbaClassDocument | VbaModuleDocument) { + const listener = new VbaTreeWalkListener(document); + const parser = this.createParser(document.textDocument); + + ParseTreeWalker.DEFAULT.walk( + listener, + parser.startRule() + ); + } + + private createParser(doc: TextDocument): VbaParser { + const lexer = new VbaLexer(new ANTLRInputStream(doc.getText())); + const parser = new VbaParser(new CommonTokenStream(lexer)); + + parser.removeErrorListeners(); + parser.addErrorListener(new VbaErrorListener()); + return parser; + } +} + + +class VbaTreeWalkListener implements vbaListener { + document: VbaClassDocument | VbaModuleDocument; + + constructor(document: VbaClassDocument | VbaModuleDocument) { + this.document = document; + } + + visitErrorNode(node: ErrorNode) { + console.log(node.payload); + } + + enterAttributeStmt = (ctx: AttributeStmtContext) => { + this.document.activeAttributeElement?.processAttribute(ctx); + }; + + enterConstStmt = (ctx: ConstStmtContext) => { + const element = new ConstDeclarationsElement(ctx, this.document.textDocument); + element.declarations.forEach((e) => this.document.registerSymbolInformation(e)); + }; + + enterEnumerationStmt = (ctx: EnumerationStmtContext) => { + const element = new EnumBlockDeclarationElement(ctx, this.document.textDocument); + this.document.registerFoldableElement(element); + this.document.registerSemanticToken(element); + this.document.registerSymbolInformation(element); + this.document.registerScopedElement(element); + }; + + exitEnumerationStmt = (_: EnumerationStmtContext) => { + this.document.deregisterScopedElement(); + }; + + enterEnumerationStmt_Constant = (ctx: EnumerationStmt_ConstantContext) => { + const element = new EnumMemberDeclarationElement(ctx, this.document.textDocument); + this.document.registerSymbolInformation(element); + this.document.registerSemanticToken(element); + }; + + enterFoldingBlockStmt = (ctx: FoldingBlockStmtContext) => { + const element = new FoldableElement(ctx, this.document.textDocument); + this.document.registerFoldableElement(element); + }; + + enterMethodStmt = (ctx: MethodStmtContext) => { + const element = new MethodBlockDeclarationElement(ctx, this.document.textDocument); + this.document.registerNamedElement(element); + this.document.registerFoldableElement(element); + this.document.registerSymbolInformation(element); + this.document.registerScopedElement(element); + }; + + exitMethodStmt = (_: MethodStmtContext) => { + this.document.deregisterScopedElement(); + }; + + enterModule = (ctx: ModuleContext) => { + const element = new ModuleElement(ctx, this.document.textDocument, this.document.symbolKind); + this.document.registerAttributeElement(element); + this.document.registerScopedElement(element); + }; + + exitModule = (_: ModuleContext) => { + const element = this.document.deregisterAttributeElement() as ModuleElement; + this.document.registerSymbolInformation(element); + this.document.deregisterScopedElement(); + this.document.deregisterAttributeElement(); + }; + + enterModuleHeader = (ctx: ModuleHeaderContext) => { + const element = new FoldableElement(ctx, this.document.textDocument); + this.document.registerFoldableElement(element); + }; + + enterVariableStmt = (ctx: VariableStmtContext) => { + const element = new VariableDeclarationsElement(ctx, this.document.textDocument); + element.declarations.forEach((e) => this.document.registerSymbolInformation(e)); + }; +} + +class VbaErrorListener extends ConsoleErrorListener { + syntaxError(recognizer: Recognizer, offendingSymbol: T, line: number, charPositionInLine: number, msg: string, e: RecognitionException | undefined): void { + super.syntaxError(recognizer, offendingSymbol, line, charPositionInLine, msg, e); + console.error(e); + if (e) { + const y = recognizer.getErrorHeader(e); + console.log(y); + } + recognizer.inputStream?.consume(); + } +} diff --git a/server/src/project/workspace.ts b/server/src/project/workspace.ts new file mode 100644 index 0000000..52a2931 --- /dev/null +++ b/server/src/project/workspace.ts @@ -0,0 +1,177 @@ +import { CompletionItem, CompletionParams, DidChangeConfigurationNotification, DidChangeConfigurationParams, DidChangeWatchedFilesParams, DocumentSymbolParams, FoldingRange, FoldingRangeParams, Hover, HoverParams, SemanticTokensParams, SemanticTokensRangeParams, SymbolInformation, TextDocuments, WorkspaceFoldersChangeEvent, _Connection } from 'vscode-languageserver'; +import { BaseProjectDocument, ProjectDocument } from './document'; +import { LanguageServerConfiguration } from '../server'; +import { hasConfigurationCapability } from '../capabilities/workspaceFolder'; +import { TextDocument } from 'vscode-languageserver-textdocument'; + + +/** + * Organises project documents and runs actions + * at a workspace level. + */ +export class Workspace { + private _events: WorkspaceEvents; + private _documents: ProjectDocument[] = []; + private _activeDocument?: ProjectDocument; + private _publicScopeDeclarations: Map = new Map(); + + readonly connection: _Connection; + + activateDocument(document: ProjectDocument) { + this._activeDocument = document; + } + + get activeDocument() { + return this._activeDocument; + } + + // constructor(connection: _Connection, capabilities: LanguageServerCapabilities) { + constructor(params: {connection: _Connection, capabilities: LanguageServerConfiguration}) { + this.connection = params.connection; + this._events = new WorkspaceEvents({ + workspace: this, + connection: params.connection, + configuration: params.capabilities, + }); + } + + /** + * Registers a declaration or pushes an ambiguous name diagnostic. + */ + registerNamedElementDeclaration(element: any) { + // Check document names for existing entry. + // Check for public scope for existing entry. + + throw new Error("Not implemented"); + } + + /** + * Pushes an overriding name diagnostic if overriding public scope. + * @param element The element to check. + */ + checkNamedElementDeclaration(element: any) { + throw new Error("Not implemented"); + } + +} + +class WorkspaceEvents { + private readonly _workspace: Workspace; + private readonly _documents: TextDocuments; + private readonly _configuration: LanguageServerConfiguration; + + activeDocument?: ProjectDocument; + + constructor(params: {connection: _Connection, workspace: Workspace, configuration: LanguageServerConfiguration}) { + this._workspace = params.workspace; + this._configuration = params.configuration; + this._documents = new TextDocuments(TextDocument); + this.initialiseConnectionEvents(params.connection); + this.initialiseDocumentsEvents(); + this._documents.listen(params.connection); + } + + private initialiseConnectionEvents(connection: _Connection) { + console.log('Initialising connection events...'); + connection.onInitialized(() => this.onInitialized()); + connection.onCompletion(params => this.onCompletion(params)); + connection.onCompletionResolve(item => this.onCompletionResolve(item)); + connection.onDidChangeConfiguration(params => this.onDidChangeConfiguration(params)); + connection.onDidChangeWatchedFiles(params => this.onDidChangeWatchedFiles(params)); + connection.onDocumentSymbol((params) => this.onDocumentSymbolAsync(params)); + connection.onHover(params => this.onHover(params)); + + if (hasConfigurationCapability(this._configuration)) { + connection.onFoldingRanges((params) => this.onFoldingRanges(params)); + } + + connection.onRequest((method: string, params: object | object[] | any) => { + switch (method) { + case 'textDocument/semanticTokens/full': { + return this.activeDocument?.languageServerSemanticTokens(); + } + case 'textDocument/semanticTokens/range': { + const rangeParams = params as SemanticTokensRangeParams; + return this.activeDocument?.languageServerSemanticTokens(rangeParams.range); + } + default: + console.error(`Unresolved request path: ${method}`); + } + }); + } + + private initialiseDocumentsEvents() { + console.log('Initialising documents events...'); + this._documents.onDidChangeContent(e => this.onDidChangeContent(e.document)); + } + + /** Connection event handlers */ + + private onCompletion(params: CompletionParams): never[] { + console.log(`onCompletion: ${params}`); + return []; + } + + private onCompletionResolve(item: CompletionItem): CompletionItem { + console.log(`onCompletionResolve: ${item.label}`); + return item; + } + + private onDidChangeConfiguration(params: DidChangeConfigurationParams): void { + console.log(`onDidChangeConfiguration: ${params}`); + } + + private onDidChangeWatchedFiles(params: DidChangeWatchedFilesParams) { + console.log(`onDidChangeWatchedFiles: ${params}`); + } + + // TODO: Should trigger a full workspace refresh. + private onDidChangeWorkspaceFolders(e: WorkspaceFoldersChangeEvent) { + console.log(`onDidChangeWorkspaceFolders: ${e}`); + this._workspace.connection.console.log(`Workspace folder change event received.\n${e}`); + } + + private async onDocumentSymbolAsync(params: DocumentSymbolParams): Promise { + console.log(`onDocumentSymbolAsync: ${params.textDocument.uri}`); + return await this.activeDocument?.languageServerSymbolInformationAsync() ?? []; + } + + private onFoldingRanges(params: FoldingRangeParams): FoldingRange[] { + const foldingRanges = this._workspace.activeDocument?.foldableElements ?? []; + console.log(`onFoldingRanges: ${params.textDocument.uri} (${foldingRanges.length} ranges)`); + return foldingRanges; + } + + private onHover(params: HoverParams): Hover { + console.log(`onHover: ${params.position.line},${params.position.character}`); + return { contents: '' }; + } + + private onInitialized(): void { + console.log('onInitialized:---'); + const connection = this._workspace.connection; + // Register for client configuration notification changes. + connection.client.register(DidChangeConfigurationNotification.type, undefined); + + // This is how we can listen for changes to workspace folders. + if (hasConfigurationCapability(this._configuration)) { + connection.workspace.onDidChangeWorkspaceFolders(e => + this.onDidChangeWorkspaceFolders(e) + ); + } + } + + /** Documents event handlers */ + + /** + * This event handler is called whenever a `TextDocuments` is changed. + * @param doc The document that changed. + */ + onDidChangeContent(doc: TextDocument) { + console.log('onDidChangeContent:--- ' + doc.uri); + this.activeDocument = BaseProjectDocument.create(this._workspace, doc); + this.activeDocument.parse(); + this._workspace.activateDocument(this.activeDocument); + } + +} \ No newline at end of file diff --git a/server/src/scope.json b/server/src/scope.json deleted file mode 100644 index 56c0b6f..0000000 --- a/server/src/scope.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "doc": { - "names": { - "MYCONSTANT": [ - "CONST", - "PUBLIC", - "String", - "literal value" - ], - "mMyPrivateVar": [ - "VAR", - "PRIVATE", - "Long" - ], - "MyFunction": [ - "FUNC", - "PUBLIC", - "String", - null, - [ - "arg1", - "arg2" - ] - ] - }, - "objects": { - "MYCONSTANT": { - "storageType": "C", - "returnType": "String", - "access": 1, //public - "literal": "some string" - }, - "mMyPrivateVar": { - "storageType": "V", - "returnType": "Long", - "access": 0 //private - }, - "MyFunction": { - "storageType": "F", - "returnType": "Long", - "access": 1, //public - "args": [ - { - "name": "arg1", - "optional": false, - "default": "nice", - "returnType": "String" - } - ] - } - }, - "scopes": { - "MyFunction": { - "names": {}, - "objects": {}, - "scopes": {} - } - } - } -} \ No newline at end of file diff --git a/server/src/scope.ts b/server/src/scope.ts deleted file mode 100644 index 915b001..0000000 --- a/server/src/scope.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { BaseDeclaration, BaseReference } from './parser/elements/memory/base'; - -class Project { - names: Map = new Map(); - scopes: Map = new Map(); - - constructor() { - // Me should never be a scope name as it's reserved. - this.scopes.set("Me", new Scope()); - } - - /** - * Attempts to find a project scope from the passed in namespace. - * @param namespace the namespace for the scope. - * @returns a project scope if found. - */ - getExplicitScope(namespace: string): Scope | undefined { - - return; - } - - getImplicitScope(namespace: string): Scope { - return this.scopes.get("Me")!; - } - - addScopedDeclaration(element: BaseDeclaration) { - // TODO: Declarations cannot override in VBA so a public / module level - // declaration that is redeclared anywhere (including method) is error. - const ns = element.isPublic ? "Me" : element.namespace.replace(/\.\w+$/, ""); - const scope = this.scopes.get(ns); - if(scope) { - scope.addDeclaration(element); - } - } -} - -class Scope { - declarations: Map = new Map(); - undeclaredReferences: BaseReference[] = []; - - addDeclaration(element: BaseDeclaration) { - const declarationName = element.identifierText; - } - - addReference(element: BaseReference) { - const declarations = this.declarations.get(element.identifierText); - if(declarations) { - declarations.forEach(declaration => { - declaration.references.push(element); - }); - } - this.undeclaredReferences.push(element); - } - - resolveReferences(globals: Map) { - this.undeclaredReferences.forEach(ref => { - const refName = ref.identifierText; - const global = globals.get(refName); - if(global) { - // Only the first declaration matters. - global[0].references.push(ref); - ref.declarationElement = global[0]; - } - }); - } -} - diff --git a/server/src/server.ts b/server/src/server.ts index 39e27b2..38a1a65 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -9,280 +9,71 @@ import { DidChangeConfigurationNotification, TextDocumentSyncKind, InitializeResult, - SemanticTokensParams, + ServerCapabilities, } from 'vscode-languageserver/node'; -// import { LanguageTools } from './capabilities/ast'; -import { activateSemanticTokenProvider } from './capabilities/vbaSemanticTokens'; -import { ProjectInformation } from './docInfo'; - -// Create a connection for the server, using Node's IPC as a transport. -// Also include all preview / proposed LSP features. -const connection = createConnection(ProposedFeatures.all); - -// Create a simple text document manager. -// const documents: TextDocuments = new TextDocuments(TextDocument); - -// Create the AST tools. -// const service = new LanguageTools(); - -let hasConfigurationCapability = false; -let hasWorkspaceFolderCapability = false; -let hasDiagnosticRelatedInformationCapability = false; - -let docInfo: ProjectInformation; - -connection.onInitialize((params: InitializeParams) => { - const capabilities = params.capabilities; - - // Does the client support the `workspace/configuration` request? - // If not, we fall back using global settings. - hasConfigurationCapability = !!( - capabilities.workspace && !!capabilities.workspace.configuration - ); - hasWorkspaceFolderCapability = !!( - capabilities.workspace && !!capabilities.workspace.workspaceFolders - ); - hasDiagnosticRelatedInformationCapability = !!( - capabilities.textDocument && - capabilities.textDocument.publishDiagnostics && - capabilities.textDocument.publishDiagnostics.relatedInformation - ); +import { Workspace } from './project/workspace'; +import { activateSemanticTokenProvider } from './capabilities/semanticTokens'; +import { activateWorkspaceFolderCapability } from './capabilities/workspaceFolder'; + + +class LanguageServer { + workspace?: Workspace; + configuration?: LanguageServerConfiguration; + readonly connection; + + constructor() { + this.connection = createConnection(ProposedFeatures.all); + this.connection.onInitialize((params: InitializeParams) => { + // Set up the workspace. + this.configuration = new LanguageServerConfiguration(params); + const workspace = new Workspace({ + connection: this.connection, + capabilities: this.configuration + }); + this.workspace = workspace; + + // Set up the connection result. + // Update this to make use of the LSCapabilities data class. + const result = new ConnectionInitializeResult(this.configuration.capabilities); + activateWorkspaceFolderCapability(params.capabilities, result); + activateSemanticTokenProvider(result); + return result; + }); + this.connection.onInitialized(() => { + // Register for client configuration notification changes. + this.connection.client.register(DidChangeConfigurationNotification.type, undefined); + + }); - docInfo = new ProjectInformation( - connection, - hasConfigurationCapability, - hasWorkspaceFolderCapability, - hasDiagnosticRelatedInformationCapability); + this.connection.listen(); + } +} - const result: InitializeResult = { - capabilities: { - textDocumentSync: TextDocumentSyncKind.Incremental, - // Tell the client that this server supports code completion. - completionProvider: { - resolveProvider: false - }, - foldingRangeProvider: true, - hoverProvider: true, - documentSymbolProvider: true - } +export class LanguageServerConfiguration { + params: InitializeParams; + capabilities: ServerCapabilities = { + hoverProvider: false, + textDocumentSync: TextDocumentSyncKind.Incremental, + completionProvider: { resolveProvider: false }, + foldingRangeProvider: true, + documentSymbolProvider: true, }; - activateSemanticTokenProvider(result); - if (hasWorkspaceFolderCapability) { - result.capabilities.workspace = { - workspaceFolders: { - supported: true - } - }; - } - return result; -}); -connection.onInitialized(() => { - if (hasConfigurationCapability) { - // Register for all configuration changes. - connection.client.register(DidChangeConfigurationNotification.type, undefined); + constructor(params: InitializeParams) { + this.params = params; } - if (hasWorkspaceFolderCapability) { - connection.workspace.onDidChangeWorkspaceFolders(e => { - connection.console.log('Workspace folder change event received.'); - }); - } -}); - -// The example settings -interface ExampleSettings { - maxNumberOfProblems: number; } -// The global settings, used when the `workspace/configuration` request is not supported by the client. -// Please note that this is not the case when using this server with the client provided in this example -// but could happen with other clients. -// const defaultSettings: ExampleSettings = { maxNumberOfProblems: 1000 }; -// let globalSettings: ExampleSettings = defaultSettings; - -// Cache the settings of all open documents -// const documentSettings: Map> = new Map(); - -// connection.onDidChangeConfiguration(change => { -// if (hasConfigurationCapability) { -// // Reset all cached document settings -// documentSettings.clear(); -// } else { -// globalSettings = ( -// (change.settings.languageServerExample || defaultSettings) -// ); -// } - -// // Revalidate all open text documents -// docInfo.docs.all().forEach(validateTextDocument); -// }); - - +class ConnectionInitializeResult implements InitializeResult { + [custom: string]: any; + capabilities: ServerCapabilities; + serverInfo?: { name: string; version?: string; }; - -// // Only keep settings for open documents -// documents.onDidClose(e => { -// documentSettings.delete(e.document.uri); -// }); - -// documents.onDidOpen(e => { -// service.evaluate(e.document); -// }); - -// // The content of a text document has changed. This event is emitted -// // when the text document first opened or when its content has changed. -// documents.onDidChangeContent(change => { -// // validateTextDocument(change.document); -// service.evaluate(change.document); -// }); - -// async function validateTextDocument(textDocument: TextDocument): Promise { -// const settings = await getDocumentSettings(textDocument.uri); -// const v = new DocumentValidator(textDocument, settings.maxNumberOfProblems); - -// service.evaluate(textDocument); - -// // Validate use of .Select -// v.validate(/\.Select/g, "Don't use `Select`. Please.", DiagnosticSeverity.Warning); - - -// // Send the computed diagnostics to VSCode. -// const diagnostics: Diagnostic[] = v.diagnostics; -// connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); -// } - -// class DocumentValidator { -// doc: TextDocument; -// text: string; -// problemLimit: integer; -// diagnostics: Diagnostic[]; - -// constructor(textDocument: TextDocument, problemLimit: integer) { -// this.doc = textDocument; -// this.text = textDocument.getText(); -// this.problemLimit = problemLimit; -// this.diagnostics = []; -// } - -// validate(pattern: RegExp, description: string, severity: DiagnosticSeverity) { -// let m: RegExpExecArray | null; -// while ((m = pattern.exec(this.text)) && this.problemLimit > 0) { -// this.problemLimit--; -// const d: Diagnostic = { -// severity: severity, -// range: { -// start: this.doc.positionAt(m.index + 1), -// end: this.doc.positionAt(m.index + m[0].length) -// }, -// message: description, -// source: 'sslinky-vba', -// code: 666 -// }; -// if (hasDiagnosticRelatedInformationCapability) { -// d.relatedInformation = [ -// { -// location: { -// uri: this.doc.uri, -// range: Object.assign({}, d.range) -// }, -// message: 'Seriously though, don\'t use it.' -// }, -// { -// location: { -// uri: this.doc.uri, -// range: Object.assign({}, d.range) -// }, -// message: 'Ever.' -// } -// ]; -// } -// this.diagnostics.push(d); -// } -// } -// } - - -// This handler provides the initial list of the completion items. -// connection.onCompletion( -// (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => { -// // The pass parameter contains the position of the text document in -// // which code complete got requested. For the example we ignore this -// // info and always provide the same completion items. -// return [ -// { -// label: 'TypeScript', -// kind: CompletionItemKind.Text, -// data: 1 -// }, -// { -// label: 'JavaScript', -// kind: CompletionItemKind.Text, -// data: 2 -// } -// ]; -// } -// ); - -// This handler resolves additional information for the item selected in -// the completion list. -// connection.onCompletionResolve( -// (item: CompletionItem): CompletionItem => { -// if (item.data === 1) { -// item.detail = 'TypeScript details'; -// item.documentation = 'TypeScript documentation'; -// } else if (item.data === 2) { -// item.detail = 'JavaScript details'; -// item.documentation = 'JavaScript documentation'; -// } -// return item; -// } -// ); - -// Make the text document manager listen on the connection -// for open, change and close text document events -// documents.listen(connection); - -// Listen on the connection -connection.listen(); - -// // Event handler for folding ranges. -// connection.onFoldingRanges((params) => { -// const doc = documents.get(params.textDocument.uri); -// if (doc) { -// return getFoldingRanges(doc, Number.MAX_VALUE); -// } -// }); - -// Event pass-through to keep server.ts simple. -connection.onDidChangeWatchedFiles(c => docInfo.onDidChangeWatchedFiles(c)); -connection.onDidChangeConfiguration((change => docInfo.onDidChangeConfiguration(change))); - -connection.onHover((params) => docInfo.getHover(params)); -connection.onCompletion((params) => docInfo.getCompletion(params)); -connection.onFoldingRanges((params) => docInfo.getFoldingRanges(params.textDocument.uri)); -connection.onDocumentSymbol((params) => docInfo.getDocumentSymbols(params.textDocument.uri)); -connection.onCompletionResolve((item) => docInfo.getCompletionResolve(item)); - -connection.onRequest((method: string, params: object | object[] | any) => { - switch (method) { - case 'textDocument/semanticTokens/full': { - const stp = params as SemanticTokensParams; - return docInfo.getSemanticTokens(stp); - } - case 'textDocument/semanticTokens/range': - return docInfo.getSemanticTokens(params); - default: - console.error(`Unresolved request path: ${method}`); + constructor(capabilities: ServerCapabilities) { + this.capabilities = capabilities; } -}); - -// connection.onCodeAction; +} -// connection.onDocumentSymbol((params) => { -// const doc = documents.get(params.textDocument.uri); -// if (doc) { -// return service.symbolProvider.Symbols(); -// } -// }); \ No newline at end of file +const languageServer = new LanguageServer(); diff --git a/server/src/utils/converters.ts b/server/src/utils/converters.ts deleted file mode 100644 index 06cfee2..0000000 --- a/server/src/utils/converters.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { SymbolKind } from 'vscode-languageserver'; - - -export function vbaTypeToSymbolConverter(t: string): SymbolKind { - switch (t.toLocaleLowerCase()) { - case 'boolean': - return SymbolKind.Boolean; - case 'single': - case 'double': - case 'currency': - case 'long': - case 'longlong': - case 'longptr': - return SymbolKind.Number; - case 'string': - return SymbolKind.String; - case 'variant': - return SymbolKind.Variable; - default: - return SymbolKind.Object; - } -} \ No newline at end of file diff --git a/server/src/utils/helpers.ts b/server/src/utils/helpers.ts index 93322bf..7e27081 100644 --- a/server/src/utils/helpers.ts +++ b/server/src/utils/helpers.ts @@ -1,90 +1,3 @@ -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { SyntaxElement } from '../parser/elements/base'; -import { ParserRuleContext } from 'antlr4ts'; -import { Range } from 'vscode-languageserver'; - -export function indexOfNearestUnder(arr: Array, n: number) { - if (arr.length === 0) { return -1; } - for (let i = 0; i < arr.length; i++) { - const result = n - arr[i]; - if (result === 0) { return i; } - if (result < 0) { return i - 1; } - } - return arr.length - 1; -} - -export function getMatchIndices(re: RegExp, text: string, maxReturned?: number): number[] { - let m = re.exec(""); - let i = 0; - const results: number[] = []; - while ((m = re.exec(text)) && i++ < (maxReturned ?? 2000)) { - results.push(groupIndex(m, 1)); - } - return results; -} - -export function stripQuotes(text: string): string { - const exp = /^"?(.*?)"?$/; - return exp.exec(text)![1]; -} - -function groupIndex(reExec: RegExpExecArray, i: number) { - return reExec.index + reExec[0].indexOf(reExec[i]); -} - -export function rangeIsChildOfElement(tr: Range, element: SyntaxElement): boolean { - const pr = element.range; - - const psl = pr.start.line; - const psc = pr.start.character; - const tsl = tr.start.line; - const tsc = tr.start.character; - - const pel = pr.end.line; - const pec = pr.end.character; - const tel = tr.end.line; - const tec = tr.end.character; - - const prStartEarlier = (psl < tsl) || (psl === tsl && psc <= tsc); - const prEndsAfter = (pel > tel) || (pel === tel && pec >= tec); - - return prStartEarlier && prEndsAfter; -} - export function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms) ); } - -/** - * Gets the document Range address of the context element. - * @param ctx the context element - * @param doc the underlying document - * @returns A Range representing an address of the context in the document. - */ -export function getCtxRange(ctx: ParserRuleContext, doc: TextDocument): Range { - const start = ctx.start.startIndex; - const stop = ctx.stop?.stopIndex ?? start; - return Range.create( - doc.positionAt(start), - doc.positionAt(stop + 1)); -} - -/** - * Represents the range as a line:char address string. - * @param r a range object. - * @returns The line/char address of the range. - */ -export function rangeAddress(r: Range): string { - const sl = r.start.line; - const el = r.end.line; - const sc = r.start.character; - const ec = r.end.character; - - if(sl==el && sc==ec) { - return `${sl}:${sc}`; - } - if(sl==el) { - return `${sl}:${sc}-${ec}`; - } - return `${sl}:${sc}-${el}:${ec}`; -} diff --git a/server/src/utils/nameTree.ts b/server/src/utils/nameTree.ts deleted file mode 100644 index 4a8fffc..0000000 --- a/server/src/utils/nameTree.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity } from 'vscode-languageserver'; -import { AmbiguousIdentifierContext, CertainIdentifierContext } from '../antlr/out/vbaParser'; -import { MethodElement, SyntaxElement, VariableDeclarationElement } from './vbaSyntaxElements'; - - -enum NameLinkType { - 'variable' = 0, - 'method' = 1, -} - -class Scope { - context: SyntaxElement; - parent?: Scope; - nameRefs: Map = new Map(); - - constructor(ctx: SyntaxElement); - constructor(ctx: SyntaxElement, pnt: Scope); - constructor(ctx: SyntaxElement, pnt?: Scope) { - this.context = ctx; - this.parent = pnt; - } - - addDeclaration(element: VariableDeclarationElement | MethodElement) { - // - } - - addRef(ctx: CertainIdentifierContext | AmbiguousIdentifierContext) { - const nameLink = this.getName(ctx.text); - // if dec - - // if not dec - } - - getName(identifier: string): NameLink { - if (!this.nameRefs.has(identifier)) { - this.nameRefs.set(identifier, new NameLink()); - } - return this.nameRefs.get(identifier)!; - } -} - -class NameLink { - type: NameLinkType = NameLinkType.variable; - - // The original decalarations that affect this name. - // 0: Variable or method not declared. - // 1: Declared once. - // 2: Multiple conflicting declarations. - declarations: SyntaxElement[] = []; - - // The places this name is referenced. - references: SyntaxElement[] = []; - diagnostics: Diagnostic[] = []; - - private diagnosticRelatedInfo: DiagnosticRelatedInformation[] = []; - - merge(link: NameLink) { - this.declarations.concat(link.declarations); - this.references.concat(link.references); - this.diagnostics.concat(link.diagnostics); - - if (link.declarations.length > 0) { - this.type = link.type; - } - } - - process(optExplicit = false) { - this.processDiagnosticRelatedInformation(); - this.validateDeclarationCount(optExplicit); - this.validateMethodSignatures(); - - this.assignSemanticTokens(); - this.assignDiagnostics(); - } - - private processDiagnosticRelatedInformation() { - this.diagnosticRelatedInfo = this.declarations - .concat(this.references) - .map((x) => DiagnosticRelatedInformation.create( - x.location(), - x.text - )); - } - - private validateDeclarationCount(optExplicit: boolean) { - if (this.declarations.length === 1) { - return; - } - - const undecSeverity = (optExplicit) ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning; - if (this.declarations.length === 0) { - if (optExplicit || this.type !== NameLinkType.variable) - this.references.forEach((x) => - this.diagnostics.push(Diagnostic.create( - x.range, - "No declaration for variable or method.", - undecSeverity, - 404, - 'VbaPro', - this.diagnosticRelatedInfo - ))); - return; - } - this.declarations.forEach((x) => - this.diagnostics.push(Diagnostic.create( - x.range, - "Ambiguous variable declaration", - DiagnosticSeverity.Error, - 500, - 'VbaPro', - this.diagnosticRelatedInfo - ))); - } - - private assignSemanticTokens() { - if (this.declarations.length === 0) { - return; - } - - this.references.forEach((x) => x.semanticToken = - this.declarations[0] - .semanticToken - ?.toNewRange(x.range)); - } - - private assignDiagnostics() { - if (this.diagnostics.length === 0) { - return; - } - const els = this.declarations.concat(this.references); - els.forEach((x) => x.addDiagnostics(this.diagnostics)); - } - - private validateMethodSignatures() { - // TODO: implement. - } -} \ No newline at end of file diff --git a/server/src/utils/vbaSyntaxElements.ts b/server/src/utils/vbaSyntaxElements.ts deleted file mode 100644 index 0d2062f..0000000 --- a/server/src/utils/vbaSyntaxElements.ts +++ /dev/null @@ -1,443 +0,0 @@ -import { ParserRuleContext } from 'antlr4ts'; -import { Diagnostic, FoldingRange, Hover, Location, Range, SemanticTokenModifiers, SemanticTokenTypes, SymbolInformation, SymbolKind, uinteger } from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { AmbiguousIdentifierContext, AttributeStmtContext, BaseTypeContext, ComplexTypeContext, ConstStmtContext, ConstSubStmtContext, EnumerationStmtContext, EnumerationStmt_ConstantContext, FunctionStmtContext, ICS_S_VariableOrProcedureCallContext, LiteralContext, ModuleContext, SubStmtContext, VariableStmtContext, VariableSubStmtContext } from '../antlr/out/vbaParser'; -import { SemanticToken } from '../capabilities/vbaSemanticTokens'; -import { vbaTypeToSymbolConverter } from './converters'; -import { stripQuotes } from './helpers'; - -export {SyntaxElement, IdentifiableSyntaxElement, UnknownElement, ModuleElement, ModuleAttribute, MethodDeclarationElement as MethodElement, EnumElement, EnumConstant, VariableDeclarationStatementElement as VariableStatementElement, VariableDeclarationElement, IdentifierElement}; - -interface SyntaxElement { - uri: string; - text: string; - range: Range; - identifier?: IdentifierElement; - context: ParserRuleContext; - symbolKind: SymbolKind; - semanticToken?: SemanticToken; - diagnostics: Diagnostic[]; - hoverText: string; - fqName?: string; - - parent?: SyntaxElement; - children: SyntaxElement[]; - - getAncestorCount(): number; - hover(): Hover | undefined; - isChildOf(element: SyntaxElement): boolean; - symbolInformation(uri: string): SymbolInformation | undefined; - foldingRange(): FoldingRange | undefined; - location(): Location; - addDiagnostics(diagnostics: Diagnostic[]): void; -} - -interface IdentifiableSyntaxElement { - identifier: IdentifierElement; - ambiguousIdentifier(): AmbiguousIdentifierContext; - ambiguousIdentifier(i: number): AmbiguousIdentifierContext; -} - -abstract class BaseElement implements SyntaxElement { - uri: string; - text: string; - range: Range; - identifier?: IdentifierElement; - context: ParserRuleContext; - symbolKind: SymbolKind; - semanticToken?: SemanticToken; - diagnostics: Diagnostic[] = []; - hoverText: string; - fqName?: string; - - parent?: SyntaxElement; - - children: SyntaxElement[] = []; - private _countAncestors = 0; - - constructor(ctx: ParserRuleContext, doc: TextDocument) { - this.uri = doc.uri; - this.context = ctx; - this.range = getCtxRange(ctx, doc); - this.text = ctx.text; - this.symbolKind = SymbolKind.Null; - this.hoverText = ''; - } - - hover = (): Hover | undefined => undefined; - location = (): Location => Location.create(this.uri, this.range); - - isChildOf(element: SyntaxElement): boolean { - const tr = this.range; - const pr = element.range; - - const psl = pr.start.line; - const psc = pr.start.character; - const tsl = tr.start.line; - const tsc = tr.start.character; - - const pel = pr.end.line; - const pec = pr.end.character; - const tel = tr.end.line; - const tec = tr.end.character; - - const prStartEarlier = (psl < tsl) || (psl === tsl && psc <= tsc); - const prEndsAfter = (pel > tel) || (pel === tel && pec >= tec); - - return prStartEarlier && prEndsAfter; - } - - symbolInformation = (uri: string): SymbolInformation | undefined => - SymbolInformation.create( - this.identifier!.text, - this.symbolKind, - this.range, - uri, - this.parent?.identifier?.text ?? ''); - - foldingRange = (): FoldingRange | undefined => - FoldingRange.create( - this.range.start.line, - this.range.end.line, - this.range.start.character, - this.range.end.character - ); - - addDiagnostics(diagnostics: Diagnostic[]) { - this.diagnostics = this.diagnostics.concat(diagnostics); - } - - // protected setIdentifierFromDoc(doc: TextDocument): void { - // if (this.isIdentifiable(this.context)) { - // const identCtx = this.context.ambiguousIdentifier(0); - // if (identCtx) { - // this.identifier = new IdentifierElement(identCtx, doc); - // } - // } - // } - - private getParent(): BaseElement | undefined { - if (this.parent) { - if (this.parent instanceof BaseElement) { - return this.parent; - } - } - } - - getAncestorCount(n = 0): number { - if (this._countAncestors === 0) { - const pnt = this.getParent(); - if (pnt) { - this._countAncestors = pnt.getAncestorCount(n + 1); - return this._countAncestors; - } - } - return this._countAncestors + n; - } - - toString = () => `${"-".repeat(this.getAncestorCount())} ${this.constructor.name}: ${this.context.text}`; -} - -class UnknownElement extends BaseElement { - -} - -class IdentifierElement extends BaseElement { - constructor(ctx: AmbiguousIdentifierContext | LiteralContext, doc: TextDocument) { - super(ctx, doc); - } - - createSemanticToken(tokType: SemanticTokenTypes, tokMods?: SemanticTokenModifiers[]) { - if (!(this.context instanceof AmbiguousIdentifierContext) && !(this.context instanceof LiteralContext)) - return; - - this.semanticToken = new SemanticToken( - this.range.start.line, - this.range.start.character, - this.text.length, - tokType, - tokMods ?? [] - ); - } -} - -class ModuleElement extends BaseElement { - constructor(ctx: ModuleContext, doc: TextDocument) { - super(ctx, doc); - this.symbolKind = SymbolKind.Module; - this.fqName = doc.uri; - } -} - -class MethodDeclarationElement extends BaseElement { - identifier: IdentifierElement; - returnType: TypeContext | undefined; - hasPrivateModifier = false; - - constructor(ctx: FunctionStmtContext | SubStmtContext, doc: TextDocument) { - super(ctx, doc); - this.hasPrivateModifier = !!(ctx.visibility()?.PRIVATE()); - this.identifier = getIdentifier(ctx, doc); - if (ctx instanceof FunctionStmtContext) { - this.returnType = this.getReturnType(doc); - this.symbolKind = SymbolKind.Function; - } else { - this.symbolKind = SymbolKind.Method; - } - } - - private getReturnType(doc: TextDocument): TypeContext | undefined { - const ctx = this.context as FunctionStmtContext; - const asTypeCtx = ctx.asTypeClause(); - if (!asTypeCtx) { return; } - const t = asTypeCtx.type_(); - const typeCtx = t.baseType() ?? t.complexType(); - if (typeCtx) { - return new TypeContext(typeCtx, doc); - } - } - - hover = () => this.getHover(); - - getHoverText(): string { - return this.text; - // const ctx = this.context as FunctionStmtContext | SubStmtContext; - // let typeHint; - // if (ctx instanceof FunctionStmtContext) { - // typeHint = ctx.typeHint(); - // } - // const mdTypeHint = typeHint?.text ?? ''; - - // const argsCtx = ctx.argList(); - - // const codeFence = '`'; - // return `${codeFence}vba\n${mdTypeHint}${this.identifier}()${codeFence}`; - } - - private getHover(): Hover { - return { - contents: this.hoverText, - range: this.range - }; - } -} - -class MethodCallElement extends BaseElement { - /** - * - */ - constructor(ctx: ICS_S_VariableOrProcedureCallContext, doc: TextDocument) { - super(ctx, doc); - - } -} - -class EnumElement extends BaseElement { - constructor(ctx: EnumerationStmtContext, doc: TextDocument) { - super(ctx, doc); - this.symbolKind = SymbolKind.Enum; - if (this.identifier) - this.identifier.createSemanticToken(SemanticTokenTypes.enum); - } -} - -class EnumConstant extends BaseElement { - foldingRange = () => undefined; - symbolInformation = (_: string) => undefined; - constructor(ctx: EnumerationStmt_ConstantContext, doc: TextDocument) { - super(ctx, doc); - this.symbolKind = SymbolKind.EnumMember; - if (this.identifier) { - this.identifier.createSemanticToken( - SemanticTokenTypes.enumMember); - } - } -} - -class VariableDeclarationStatementElement extends BaseElement { - variableList: VariableDeclarationElement[] = []; - hasPrivateModifier = false; - foldingRange = () => undefined; - - constructor(ctx: VariableStmtContext | ConstStmtContext, doc: TextDocument) { - super(ctx, doc); - this.symbolKind = SymbolKind.Variable; - this.hasPrivateModifier = !!(ctx.visibility()?.PRIVATE()); - this.resolveDeclarations(ctx, doc); - } - - hover = (): Hover => ({ - contents: this.hoverText, - range: this.range - }); - - getHoverText(): string { - return this.text; - } - - private resolveDeclarations(ctx: VariableStmtContext | ConstStmtContext, doc: TextDocument) { - if (ctx instanceof VariableStmtContext) { - this.resolveVarDeclarations(ctx, doc); - } else { - this.resolveConstDeclarations(ctx, doc); - } - } - - private resolveVarDeclarations(ctx: VariableStmtContext, doc: TextDocument) { - const tokenMods = this.getSemanticTokenModifiers(ctx); - ctx.variableListStmt().variableSubStmt().forEach((x) => - this.variableList.push(new VariableDeclarationElement(x, doc, tokenMods, this.hasPrivateModifier))); - } - - private resolveConstDeclarations(ctx: ConstStmtContext, doc: TextDocument) { - const tokenMods = this.getSemanticTokenModifiers(ctx); - ctx.constSubStmt().forEach((x) => this.variableList.push( - new VariableDeclarationElement(x, doc, tokenMods, this.hasPrivateModifier))); - } - - private getSemanticTokenModifiers(ctx: VariableStmtContext | ConstStmtContext): SemanticTokenModifiers[] { - const result: SemanticTokenModifiers[] = [SemanticTokenModifiers.declaration]; - if (ctx instanceof VariableStmtContext && ctx.STATIC()) - result.push(SemanticTokenModifiers.static); - if (ctx instanceof ConstStmtContext) - result.push(SemanticTokenModifiers.readonly); - return result; - } -} - -class VariableDeclarationElement extends BaseElement { - asType: TypeContext | undefined; - identifier: IdentifierElement; - hasPrivateModifier = false; - - constructor(ctx: VariableSubStmtContext | ConstSubStmtContext, doc: TextDocument, tokenModifiers: SemanticTokenModifiers[], hasPrivateModifier: boolean) { - super(ctx, doc); - this.identifier = getIdentifier(ctx, doc); - this.hasPrivateModifier = hasPrivateModifier; - this.asType = TypeContext.create(ctx, doc); - this.symbolKind = vbaTypeToSymbolConverter(this.asType?.text ?? 'variant'); - this.createSemanticToken(tokenModifiers); - } - - private createSemanticToken(tokenModifiers: SemanticTokenModifiers[]) { - const name = this.identifier!; - this.semanticToken = new SemanticToken( - name.range.start.line, - name.range.start.character, - name.text.length, - SemanticTokenTypes.variable, - tokenModifiers - ); - } -} - -// class VariableElement extends BaseElement { -// members: MemberElement[] = []; - -// constructor(ctx: ICS_S_VariableOrProcedureCallContext, doc: TextDocument) { -// super(ctx, doc); - -// } -// } - -// class ValueElement extends BaseElement { -// method?: MethodCallElement; -// variable?: VariableElement; - - -// constructor(ctx: ValueStmtContext, doc: TextDocument) { -// super(ctx, doc); - -// } -// } - -// class VariableAssignElement extends BaseElement { -// leftElement: VariableElement; -// rightElement: ValueElement; - -// constructor(ctx: LetStmtContext | SetStmtContext, doc: TextDocument) { -// super(ctx, doc); - -// this.leftElement = this.getLeftHandVariable(doc); -// this.rightElement = new ValueElement(ctx.valueStmt(), doc); - -// const callStmtContext = ctx.implicitCallStmt_InStmt(); -// const varOrProc = callStmtContext.iCS_S_VariableOrProcedureCall(); -// const isProcedure = !!varOrProc?.subscripts(); -// if (isProcedure) { -// // -// } else { -// this.rightElement = getVariableAssignment(ctx.valueStmt(), doc); -// } -// } - -// private getLeftHandVariable(doc: TextDocument): VariableElement { -// const ctx = this.context as LetStmtContext | SetStmtContext; -// const varElement = ctx.implicitCallStmt_InStmt().iCS_S_VariableOrProcedureCall(); -// if (!varElement) { -// throw new Error('Context is not a variable.'); -// } -// return new VariableElement(varElement, doc); -// } - -// private getFromVpc(ctx: ICS_S_VariableOrProcedureCallContext, doc: TextDocument): VariableElement | MethodCallElement { -// if (ctx.subscripts()) { -// return new MethodCallElement(ctx, doc); -// } -// return new VariableElement(ctx, doc); -// } - -// symbolInformation = (_: string) => undefined; -// } - -class ModuleAttribute { - identifier?: IdentifierElement; - literal?: IdentifierElement; - - key = (): string => this.identifier?.text ?? 'Undefined'; - value = (): string => (this.literal) ? stripQuotes(this.literal.text) : 'Undefined'; - - constructor(ctx: AttributeStmtContext, doc: TextDocument) { - const idCtx = ctx - .implicitCallStmt_InStmt() - ?.iCS_S_VariableOrProcedureCall() - ?.ambiguousIdentifier(); - - if (idCtx) { - this.identifier = new IdentifierElement(idCtx, doc); - this.literal = new IdentifierElement(idCtx, doc); - } - } -} - -class TypeContext extends BaseElement { - constructor(ctx: BaseTypeContext | ComplexTypeContext, doc: TextDocument) { - super(ctx, doc); - } - - static create(ctx: VariableSubStmtContext | ConstSubStmtContext, doc: TextDocument): TypeContext | undefined { - const xCtx = ctx.asTypeClause()?.type_(); - const tCtx = xCtx?.baseType() ?? xCtx?.complexType(); - if (tCtx) { return new TypeContext(tCtx, doc); } - } -} - -function getCtxRange(ctx: ParserRuleContext, doc: TextDocument): Range { - const start = ctx.start.startIndex; - const stop = ctx.stop?.stopIndex ?? start; - return Range.create( - doc.positionAt(start), - doc.positionAt(stop + 1)); -} - -function getIdentifier(ctx: ParserRuleContext, doc: TextDocument): IdentifierElement { - if (isIdentifiable(ctx)) { - return new IdentifierElement( - ctx.ambiguousIdentifier(0), doc); - } - throw new Error('Not an identifiable context'); -} - -function isIdentifiable(o: any): o is IdentifiableSyntaxElement { - return 'ambiguousIdentifier' in o; -} diff --git a/server/src/utils/vbaSyntaxParser.ts b/server/src/utils/vbaSyntaxParser.ts deleted file mode 100644 index 4ef5535..0000000 --- a/server/src/utils/vbaSyntaxParser.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { ANTLRInputStream, CommonTokenStream, ConsoleErrorListener, RecognitionException, Recognizer } from 'antlr4ts'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { vbaListener } from '../antlr/out/vbaListener'; -import { AmbiguousIdentifierContext, AttributeStmtContext, CertainIdentifierContext, ConstStmtContext, EnumerationStmtContext, EnumerationStmt_ConstantContext, FunctionStmtContext, ImplicitCallStmt_InBlockContext, ImplicitCallStmt_InStmtContext, LetStmtContext, ModuleContext, ModuleHeaderContext, SetStmtContext, StartRuleContext, SubStmtContext, UnknownLineContext, VariableStmtContext, vbaParser } from '../antlr/out/vbaParser'; -import { vbaLexer as VbaLexer } from '../antlr/out/vbaLexer'; -import { ParseTreeWalker } from 'antlr4ts/tree/ParseTreeWalker'; -import { ErrorNode } from 'antlr4ts/tree/ErrorNode'; -import { MethodElement, ModuleAttribute, ModuleElement, SyntaxElement, EnumElement, EnumConstant as EnumConstantElement, VariableStatementElement, VariableDeclarationElement, IdentifierElement } from './vbaSyntaxElements'; -import { SymbolKind } from 'vscode-languageserver'; - - -export interface ResultsContainer { - module?: ModuleElement; - addModule(element: ModuleElement): void; - addElement(element: SyntaxElement): void; - setModuleAttribute(attr: ModuleAttribute): void; - addScopedReference(emt: IdentifierElement): void; - addScopedDeclaration(emt: MethodElement | VariableDeclarationElement): void; - pushScopeElement(emt: MethodElement): void; - popScopeElement(): void; -} - - -export class SyntaxParser { - parse(doc: TextDocument, resultsContainer: ResultsContainer) { - const listener = new VbaTreeWalkListener(doc, resultsContainer); - const parserEntry = this.getParseEntryPoint(doc); - - ParseTreeWalker.DEFAULT.walk( - listener, - parserEntry - ); - } - - private getParseEntryPoint(doc: TextDocument): StartRuleContext { - const lexer = new VbaLexer(new ANTLRInputStream(doc.getText())); - const parser = new vbaParser(new CommonTokenStream(lexer)); - - parser.removeErrorListeners(); - parser.addErrorListener(new VbaErrorListener()); - return parser.startRule(); - } -} - -class VbaTreeWalkListener implements vbaListener { - doc: TextDocument; - resultsContainer: ResultsContainer; - - constructor(doc: TextDocument, resultsContainer: ResultsContainer) { - this.doc = doc; - this.resultsContainer = resultsContainer; - } - - visitErrorNode(node: ErrorNode) { - console.log(node.payload); - } - - enterUnknownLine = (ctx: UnknownLineContext) => { - console.log(ctx.text); - }; - - enterModule = (ctx: ModuleContext) => - this.resultsContainer.addModule( - new ModuleElement(ctx, this.doc)); - - // Only classes have a header. TODO: Test this for forms. - enterModuleHeader = (_: ModuleHeaderContext) => - this.resultsContainer.module!.symbolKind = SymbolKind.Class; - - enterAttributeStmt = (ctx: AttributeStmtContext) => { - const attr = new ModuleAttribute(ctx, this.doc); - this.resultsContainer.setModuleAttribute(attr); - if (attr.identifier?.text === 'VB_Name') { - const module = this.resultsContainer.module; - if (module) { - module.identifier = attr.literal; - } - } - }; - - enterSubStmt = (ctx: SubStmtContext) => - this.resultsContainer.addElement( - new MethodElement(ctx, this.doc)); - - exitSubStmt = (_: SubStmtContext) => - this.resultsContainer.popScopeElement(); - - enterFunctionStmt = (ctx: FunctionStmtContext) => { - const e = new MethodElement(ctx, this.doc); - this.resultsContainer.addScopedDeclaration(e); - }; - - exitFunctionStmt = (_: FunctionStmtContext) => - this.resultsContainer.popScopeElement(); - - enterEnumerationStmt = (ctx: EnumerationStmtContext) => - this.resultsContainer.addElement( - new EnumElement(ctx, this.doc)); - - enterEnumerationStmt_Constant = (ctx: EnumerationStmt_ConstantContext) => - this.resultsContainer.addElement( - new EnumConstantElement(ctx, this.doc)); - - enterVariableStmt = (ctx: VariableStmtContext) => this.enterVarOrConstStmt(ctx); - enterConstStmt = (ctx: ConstStmtContext) => this.enterVarOrConstStmt(ctx); - - private enterVarOrConstStmt(ctx: VariableStmtContext | ConstStmtContext) { - const declaration = new VariableStatementElement(ctx, this.doc); - this.resultsContainer.addElement(declaration); - declaration.variableList.forEach((v) => this.resultsContainer.addScopedDeclaration(v)); - } - - // enterImplicitCallStmt_InStmt = (ctx: ImplicitCallStmt_InStmtContext) => this.enterImplicitCallStmt(ctx); - // enterImplicitCallStmt_InBlock = (ctx: ImplicitCallStmt_InBlockContext) => this.enterImplicitCallStmt(ctx); - - // private enterImplicitCallStmt(ctx: ImplicitCallStmt_InStmtContext | ImplicitCallStmt_InBlockContext) { - // // console.log('imp call ' + ctx.text); - // } - - enterCertainIdentifier = (ctx: CertainIdentifierContext) => this.enterIdentifier(ctx); - enterAmbiguousIdentifier = (ctx: AmbiguousIdentifierContext) => this.enterIdentifier(ctx); - - private enterIdentifier(ctx: CertainIdentifierContext | AmbiguousIdentifierContext) { - const ident = new IdentifierElement(ctx, this.doc); - this.resultsContainer.addScopedReference(ident); - } - - // enterLetStmt = (ctx: LetStmtContext) => this.enterAssignStmt(ctx); - // enterSetStmt = (ctx: SetStmtContext) => this.enterAssignStmt(ctx); - - // private enterAssignStmt(ctx: LetStmtContext | SetStmtContext) { - // const assignment = new VariableAssignElement(ctx, this.doc); - // this.resultsContainer.addScopedReference(assignment); - // } -} - -class VbaErrorListener extends ConsoleErrorListener { - syntaxError(recognizer: Recognizer, offendingSymbol: T, line: number, charPositionInLine: number, msg: string, e: RecognitionException | undefined): void { - super.syntaxError(recognizer, offendingSymbol, line, charPositionInLine, msg, e); - console.error(e); - if (e) { - const y = recognizer.getErrorHeader(e); - console.log(y); - } - recognizer.inputStream?.consume(); - } -} \ No newline at end of file diff --git a/snippets/vba.json b/snippets/vba.json index 3c7f9df..ddcc259 100644 --- a/snippets/vba.json +++ b/snippets/vba.json @@ -6,6 +6,7 @@ "Private m${1:PropertyName} As ${2:PropertyType}", "Public Property Let $1(var As $2)", "Attribute $1.VB_Description = \"${3:Dosctring}.\"", + "' $3.", " m$1 = var", "End Property", "", @@ -16,7 +17,6 @@ "$0" ] }, - "Public Property Set": { "prefix": "propset", "description": "Property set and get with backing store", @@ -24,42 +24,54 @@ "Private m${1:PropertyName} As ${2:PropertyType}", "Public Property Set $1(var As $2)", "Attribute $1.VB_Description = \"${3:Dosctring}.\"", - " Set m$1 = var", + "' $3.", + " Set m$1 = var", "End Property", "", "Public Property Get $1() As $2", - " Set $1 = m$1", + " Set $1 = m$1", "End Property", "", "$0" ] }, - + "Public Property Get": { + "prefix": "propget", + "description": "Property get only", + "body": [ + "Public Property Get ${1:PropertyName}() As ${2:PropertyType}", + "Attribute $1.VB_Description = \"${3:Dosctring}.\"", + "' $3.", + " $0", + "End Property", + "" + ] + }, "Subroutine": { "prefix": "sub", "description": "Subroutine", "body": [ "${1:Public }Sub ${2:Identifier}($3)", "Attribute $2.VB_Description = \"${4:Dosctring}.\"", - "' $4.", + "' $4.", "'", "' Args:", "' param1:", "'", "' Raises:", "'", + "", " $0", "End Sub" ] }, - "Function": { "prefix": "func", - "description": "Subroutine", + "description": "Function", "body": [ "${1:Public }Function ${2:Identifier}($3) As $4", "Attribute $2.VB_Description = \"${5:Dosctring}.\"", - "' $5.", + "' $5.", "'", "' Args:", "' param1:", @@ -68,13 +80,104 @@ "'", "' Raises:", "'", + " Dim result As $4", " $0", + "", + " $2 = result", "End Function" ] }, - + "EnumExcl": { + "prefix": "enumexcl", + "description": "Exclusive Enum", + "body": [ + "Public Enum ${1:Identifier}", + " ${2:Enum1}", + " ${3:Enum2}", + " ${4:Enum3}", + "End Enum" + ] + }, + "EnumIncl": { + "prefix": "enumincl", + "description": "Inclusive Enum", + "body": [ + "Public Enum ${1:Identifier}", + " ${2:Enum1} = 2 ^^ 1", + " ${3:Enum2} = 2 ^^ 2", + " ${4:Enum3} = 2 ^^ 3", + "End Enum" + ] + }, + "ForLoop": { + "prefix": "fori", + "description": "For Loop", + "body": [ + "${4:Dim $1 As Long}", + "For ${1:i} = ${2:low} To ${3:high}", + " $0", + "Next $1" + ] + }, + "ForEachLoop": { + "prefix": "foreach", + "description": "For Each Loop", + "body": [ + "For Each ${1:object} In ${2:collection}", + " $0", + "Next $1" + ] + }, + "If": { + "prefix": "ifblock", + "description": "If Block", + "body": [ + "If ${1:condition} Then", + " $0", + "End If" + ] + }, + "IfElse": { + "prefix": "ifelse", + "description": "If Else Block", + "body": [ + "If ${1:condition} Then", + " $0", + "Else", + "End If" + ] + }, + "TypeCheckAssignment": { + "prefix": "typecheck", + "description": "Object type check assignment", + "body": [ + "If IsObject(${1:obj?}) Then", + " Set ${2:Assignment}", + "Else", + " $2", + "End If$0" + ] + }, + "Case": { + "prefix": "case", + "description": "Select Case", + "body": [ + "Select Case $1", + " Case Is = $2:", + " $0", + " Case Else:", + "End Select" + ] + }, + "Event": { + "prefix": "event", + "description": "Event", + "body": [ + "Public Event ${1:Identifier}($3)" + ] + }, "Constructor": { - "prefix": "init", + "prefix": "ctor", "description": "Class_Initialize", "body": [ "Private Sub Class_Initialize()", @@ -82,7 +185,15 @@ "End Sub" ] }, - + "Destructor": { + "prefix": "dtor", + "description": "Class_Terminate", + "body": [ + "Private Sub Class_Terminate()", + " $0", + "End Sub" + ] + }, "Base Class Template": { "prefix": "class", "description": "Basic class", @@ -92,17 +203,46 @@ " MultiUse = -1 'True", "END", "Attribute VB_Name = \"${1:ClassName}\"", + "Attribute VB_Description = \"${2:Class description goes here}\"", "Attribute VB_GlobalNameSpace = False", "Attribute VB_Creatable = False", "Attribute VB_PredeclaredId = False", "Attribute VB_Exposed = False", + "' Copyright 2024 Sam Vanderslink", + "' ", + "' Permission is hereby granted, free of charge, to any person obtaining a copy ", + "' of this software and associated documentation files (the \"Software\"), to deal ", + "' in the Software without restriction, including without limitation the rights ", + "' to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ", + "' copies of the Software, and to permit persons to whom the Software is ", + "' furnished to do so, subject to the following conditions:", + "' ", + "' The above copyright notice and this permission notice shall be included in ", + "' all copies or substantial portions of the Software.", + "' ", + "' THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ", + "' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ", + "' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ", + "' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ", + "' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING ", + "' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS ", + "' IN THE SOFTWARE.", + "", "Option Explicit", "$0", "'-------------------------------------------------------------------------------", "' Class: $1", - "' ${2:Class description goes here}", + "' $2", + "'-------------------------------------------------------------------------------", + "", + "' Enums", + "'-------------------------------------------------------------------------------", + "", + "", + "' Events", "'-------------------------------------------------------------------------------", "", + "", "' Private Backing Store", "'-------------------------------------------------------------------------------", "", @@ -133,39 +273,56 @@ "" ] }, - - "Select Case": { - "prefix": "case", - "description": "Select Case", - "body": [ - "Select Case $1", - " Case Is = $2:", - " $0", - " Case Else:", - "End Select" - ] - }, - - "Enum": { - "prefix": "enum", - "description": "Enum", + "Base Module Template": { + "prefix": "module", + "description": "Basic module", "body": [ - "Public Enum ${1:Name}", - " ${2:Option}", - "End Enum" + "Attribute VB_Name = \"${1:ModuleName}\"", + "' Copyright 2024 Sam Vanderslink", + "' ", + "' Permission is hereby granted, free of charge, to any person obtaining a copy ", + "' of this software and associated documentation files (the \"Software\"), to deal ", + "' in the Software without restriction, including without limitation the rights ", + "' to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ", + "' copies of the Software, and to permit persons to whom the Software is ", + "' furnished to do so, subject to the following conditions:", + "' ", + "' The above copyright notice and this permission notice shall be included in ", + "' all copies or substantial portions of the Software.", + "' ", + "' THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ", + "' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ", + "' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ", + "' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ", + "' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING ", + "' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS ", + "' IN THE SOFTWARE.", + "", + "Option Explicit", + "$0" ] }, - - "EnumMulti": { - "prefix": "enummulti", - "description": "Enum with multi select", + "Unit Test Template": { + "prefix": "test", + "description": "Unit test basic template", "body": [ - "Public Enum ${1:Name}", - " ${2:Option} = 2", - " ${3:Option} = 4", - " ${4:Option} = 8", - " ${5:Option} = 16", - "End Enum" + "Private Function TestList_${1:Identifier}() As TestResult", + "Attribute TestList_$1.VB_Description = \"${2:Dosctring}.\"", + "' $2.", + " Dim tr As New TestResult", + "", + "' Arrange", + " $0", + "", + "' Act", + "", + "", + "' Assert", + "", + "", + "Finally:", + " Set TestList_$1 = tr", + "End Function" ] } -} \ No newline at end of file +}