diff --git a/client/scripts/bundle.js b/client/scripts/bundle.js index 0d274115..d7cc443c 100755 --- a/client/scripts/bundle.js +++ b/client/scripts/bundle.js @@ -160,6 +160,7 @@ async function main() { external: [ "vscode", "@nomicfoundation/solidity-analyzer", + "@nomicfoundation/slang", "fsevents", "mocha", ], @@ -198,6 +199,17 @@ async function main() { "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.1", "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.1", "@nomicfoundation/solidity-analyzer-freebsd-x64": "0.1.1", + + "@nomicfoundation/slang": "0.10.1", + "@nomicfoundation/slang-darwin-arm64": "0.10.1", + "@nomicfoundation/slang-win32-arm64-msvc": "0.10.1", + "@nomicfoundation/slang-linux-arm64-gnu": "0.10.1", + "@nomicfoundation/slang-linux-arm64-musl": "0.10.1", + "@nomicfoundation/slang-win32-ia32-msvc": "0.10.1", + "@nomicfoundation/slang-darwin-x64": "0.10.1", + "@nomicfoundation/slang-win32-x64-msvc": "0.10.1", + "@nomicfoundation/slang-linux-x64-gnu": "0.10.1", + "@nomicfoundation/slang-linux-x64-musl": "0.10.1", }, }) ); diff --git a/flags.json b/flags.json new file mode 100644 index 00000000..be52c528 --- /dev/null +++ b/flags.json @@ -0,0 +1,9 @@ +{ + "documentSymbol": { + "percent": 0 + }, + + "semanticHighlighting": { + "percent": 0 + } +} diff --git a/package-lock.json b/package-lock.json index 48ddd7ff..d4de1967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,6 @@ "packages": { "": { "name": "solidity-language-server-monorepo", - "hasInstallScript": true, "workspaces": [ "client", "server", @@ -2661,6 +2660,160 @@ "setimmediate": "^1.0.5" } }, + "node_modules/@nomicfoundation/slang": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-0.10.1.tgz", + "integrity": "sha512-qU9eHCExF6Hix4KPXHv1oQN1vWYGeOrHJXZ+uxroCMZ4Tf5P/kSrxVI2SqnVmwpSdxrwJdDQ9cI8Fe6PV7YDCA==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@nomicfoundation/slang-darwin-arm64": "0.10.1", + "@nomicfoundation/slang-darwin-x64": "0.10.1", + "@nomicfoundation/slang-linux-arm64-gnu": "0.10.1", + "@nomicfoundation/slang-linux-arm64-musl": "0.10.1", + "@nomicfoundation/slang-linux-x64-gnu": "0.10.1", + "@nomicfoundation/slang-linux-x64-musl": "0.10.1", + "@nomicfoundation/slang-win32-arm64-msvc": "0.10.1", + "@nomicfoundation/slang-win32-ia32-msvc": "0.10.1", + "@nomicfoundation/slang-win32-x64-msvc": "0.10.1" + } + }, + "node_modules/@nomicfoundation/slang-darwin-arm64": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-darwin-arm64/-/slang-darwin-arm64-0.10.1.tgz", + "integrity": "sha512-lvXEs9qQS5Qm0vUhy5NwTlvkTMH7cbm2//Z80jhaBqIJlQFUW/0A4JqK/9RAji8ZuOtGC2QT9LnOk2VoNQGiwg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-darwin-x64": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-darwin-x64/-/slang-darwin-x64-0.10.1.tgz", + "integrity": "sha512-YV8OZyA++MZebNc5j62bXvQKKT8x6jg5kR2O332HHLZb/5kYWJ0PQYw61W9/DwnFFhBwVT65AZ+cJdM0LQJxog==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-arm64-gnu": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-arm64-gnu/-/slang-linux-arm64-gnu-0.10.1.tgz", + "integrity": "sha512-6vJmGMa6yvrf5PvO9lxnd5mfFtrP+1eTVhNBjbAazaSFKfpDrjR4b29JyLwREcialj/+L3prvEUjXRchIj1Gqg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-arm64-musl": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-arm64-musl/-/slang-linux-arm64-musl-0.10.1.tgz", + "integrity": "sha512-cPX5ybj6A46mb9pros8Kwpzv/Lpkp2q+gS+s8Zg62NkogvSqswDZDlBAZ7FUZTzGpMfhZJn75itPgGUCAoImUA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-x64-gnu": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-x64-gnu/-/slang-linux-x64-gnu-0.10.1.tgz", + "integrity": "sha512-sZvtSgb5LencgjqfLkFlhrYQgM8QoSy2t0IcCG1JLA8MXWlYRN/F3nDm3zKSmuOwmtFIzvbc7X/wkkGCNsSQ9g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-x64-musl": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-x64-musl/-/slang-linux-x64-musl-0.10.1.tgz", + "integrity": "sha512-S2WnY4tMCWBPaAQcPoRDZ6M01hKgE1R22n7sqnF7KI2o0Khsm9fnlAZ/2ct4ioe2Je4hyHrNhms8/534P8dQuw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-arm64-msvc": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-arm64-msvc/-/slang-win32-arm64-msvc-0.10.1.tgz", + "integrity": "sha512-diu8P/UdSdn1p+XKQK5NgZV2RTZWsL2kHLO+n0bZ3Z4lTojL8drDhMXIXzMcwRUuJdXv/NkO97dsBwM+CyhGiw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-ia32-msvc": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-ia32-msvc/-/slang-win32-ia32-msvc-0.10.1.tgz", + "integrity": "sha512-oRNDHADhWFq6D2l8UacJ+ExxJO7sYKOMn4IlYGulyp2KGzjnk4fl2vCpYVUVJ+y9pF4W2G1G06nxxddaPk/2mg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-x64-msvc": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-x64-msvc/-/slang-win32-x64-msvc-0.10.1.tgz", + "integrity": "sha512-1a6ip/OkkJ8rMaMupgmbon0Rc2rgZ4e+rC7rDA3E2VbDjZsCd2bHAOop9bQCO2rUA9pIYkzwueWJkJZGTKs82w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nomicfoundation/solidity-analyzer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", @@ -11808,6 +11961,7 @@ "version": "0.7.3", "license": "MIT", "dependencies": { + "@nomicfoundation/slang": "^0.10.1", "@nomicfoundation/solidity-analyzer": "0.1.1" }, "bin": { @@ -14561,6 +14715,76 @@ } } }, + "@nomicfoundation/slang": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-0.10.1.tgz", + "integrity": "sha512-qU9eHCExF6Hix4KPXHv1oQN1vWYGeOrHJXZ+uxroCMZ4Tf5P/kSrxVI2SqnVmwpSdxrwJdDQ9cI8Fe6PV7YDCA==", + "requires": { + "@nomicfoundation/slang-darwin-arm64": "0.10.1", + "@nomicfoundation/slang-darwin-x64": "0.10.1", + "@nomicfoundation/slang-linux-arm64-gnu": "0.10.1", + "@nomicfoundation/slang-linux-arm64-musl": "0.10.1", + "@nomicfoundation/slang-linux-x64-gnu": "0.10.1", + "@nomicfoundation/slang-linux-x64-musl": "0.10.1", + "@nomicfoundation/slang-win32-arm64-msvc": "0.10.1", + "@nomicfoundation/slang-win32-ia32-msvc": "0.10.1", + "@nomicfoundation/slang-win32-x64-msvc": "0.10.1" + } + }, + "@nomicfoundation/slang-darwin-arm64": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-darwin-arm64/-/slang-darwin-arm64-0.10.1.tgz", + "integrity": "sha512-lvXEs9qQS5Qm0vUhy5NwTlvkTMH7cbm2//Z80jhaBqIJlQFUW/0A4JqK/9RAji8ZuOtGC2QT9LnOk2VoNQGiwg==", + "optional": true + }, + "@nomicfoundation/slang-darwin-x64": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-darwin-x64/-/slang-darwin-x64-0.10.1.tgz", + "integrity": "sha512-YV8OZyA++MZebNc5j62bXvQKKT8x6jg5kR2O332HHLZb/5kYWJ0PQYw61W9/DwnFFhBwVT65AZ+cJdM0LQJxog==", + "optional": true + }, + "@nomicfoundation/slang-linux-arm64-gnu": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-arm64-gnu/-/slang-linux-arm64-gnu-0.10.1.tgz", + "integrity": "sha512-6vJmGMa6yvrf5PvO9lxnd5mfFtrP+1eTVhNBjbAazaSFKfpDrjR4b29JyLwREcialj/+L3prvEUjXRchIj1Gqg==", + "optional": true + }, + "@nomicfoundation/slang-linux-arm64-musl": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-arm64-musl/-/slang-linux-arm64-musl-0.10.1.tgz", + "integrity": "sha512-cPX5ybj6A46mb9pros8Kwpzv/Lpkp2q+gS+s8Zg62NkogvSqswDZDlBAZ7FUZTzGpMfhZJn75itPgGUCAoImUA==", + "optional": true + }, + "@nomicfoundation/slang-linux-x64-gnu": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-x64-gnu/-/slang-linux-x64-gnu-0.10.1.tgz", + "integrity": "sha512-sZvtSgb5LencgjqfLkFlhrYQgM8QoSy2t0IcCG1JLA8MXWlYRN/F3nDm3zKSmuOwmtFIzvbc7X/wkkGCNsSQ9g==", + "optional": true + }, + "@nomicfoundation/slang-linux-x64-musl": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-x64-musl/-/slang-linux-x64-musl-0.10.1.tgz", + "integrity": "sha512-S2WnY4tMCWBPaAQcPoRDZ6M01hKgE1R22n7sqnF7KI2o0Khsm9fnlAZ/2ct4ioe2Je4hyHrNhms8/534P8dQuw==", + "optional": true + }, + "@nomicfoundation/slang-win32-arm64-msvc": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-arm64-msvc/-/slang-win32-arm64-msvc-0.10.1.tgz", + "integrity": "sha512-diu8P/UdSdn1p+XKQK5NgZV2RTZWsL2kHLO+n0bZ3Z4lTojL8drDhMXIXzMcwRUuJdXv/NkO97dsBwM+CyhGiw==", + "optional": true + }, + "@nomicfoundation/slang-win32-ia32-msvc": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-ia32-msvc/-/slang-win32-ia32-msvc-0.10.1.tgz", + "integrity": "sha512-oRNDHADhWFq6D2l8UacJ+ExxJO7sYKOMn4IlYGulyp2KGzjnk4fl2vCpYVUVJ+y9pF4W2G1G06nxxddaPk/2mg==", + "optional": true + }, + "@nomicfoundation/slang-win32-x64-msvc": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-x64-msvc/-/slang-win32-x64-msvc-0.10.1.tgz", + "integrity": "sha512-1a6ip/OkkJ8rMaMupgmbon0Rc2rgZ4e+rC7rDA3E2VbDjZsCd2bHAOop9bQCO2rUA9pIYkzwueWJkJZGTKs82w==", + "optional": true + }, "@nomicfoundation/solidity-analyzer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", @@ -14640,6 +14864,7 @@ "version": "file:server", "requires": { "@istanbuljs/nyc-config-typescript": "1.0.2", + "@nomicfoundation/slang": "^0.10.1", "@nomicfoundation/solidity-analyzer": "0.1.1", "@sentry/node": "7.32.1", "@sentry/tracing": "7.32.1", diff --git a/package.json b/package.json index 1db18def..5a151564 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "url": "https://github.com/NomicFoundation/hardhat-vscode/issues" }, "scripts": { - "postinstall": "npm install --no-save --ignore-scripts --force @nomicfoundation/solidity-analyzer-win32-ia32-msvc@0.1.1", "build": "tsc -b ./client/tsconfig.json && tsc -b ./server/tsconfig.build.json && tsc -b ./coc/tsconfig.json && tsc -b", "watch": "concurrently -n client,server \"tsc -b -w ./client/tsconfig.json\" \"tsc -b -w ./server/tsconfig.build.json\"", "test:unit": "npm -w server run test", diff --git a/server/package.json b/server/package.json index 5a0861f4..eac52bdf 100644 --- a/server/package.json +++ b/server/package.json @@ -56,6 +56,7 @@ "@types/qs": "^6.9.7", "@types/semver": "^7.3.12", "@types/sinon": "10.0.6", + "@types/yaml": "^1.9.7", "c3-linearization": "0.3.0", "chai": "4.3.4", "codecov": "3.8.3", @@ -84,10 +85,10 @@ "vscode-languageserver-textdocument": "1.0.8", "vscode-languageserver-types": "3.17.3", "vscode-uri": "3.0.7", - "@types/yaml": "^1.9.7", "yaml": "^2.2.1" }, "dependencies": { + "@nomicfoundation/slang": "^0.10.1", "@nomicfoundation/solidity-analyzer": "0.1.1" } } diff --git a/server/scripts/bundle.js b/server/scripts/bundle.js index fab71131..dba87171 100644 --- a/server/scripts/bundle.js +++ b/server/scripts/bundle.js @@ -86,7 +86,12 @@ async function main() { minifyWhitespace: true, minifyIdentifiers: false, minifySyntax: true, - external: ["@nomicfoundation/solidity-analyzer", "fsevents", "mocha"], + external: [ + "@nomicfoundation/solidity-analyzer", + "@nomicfoundation/slang", + "fsevents", + "mocha", + ], platform: "node", outdir: ".", logLevel: "info", diff --git a/server/src/parser/slangHelpers.ts b/server/src/parser/slangHelpers.ts new file mode 100644 index 00000000..00da758d --- /dev/null +++ b/server/src/parser/slangHelpers.ts @@ -0,0 +1,62 @@ +import { NodeType, RuleNode, TokenNode } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { TextRange } from "@nomicfoundation/slang/text_index"; +import _ from "lodash"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import { Range } from "vscode-languageserver-types"; +import { Language } from "@nomicfoundation/slang/language"; +import semver from "semver"; +import { getPlatform } from "../utils/operatingSystem"; + +export type SlangNode = RuleNode | TokenNode; +export type NodeKind = RuleKind | TokenKind; + +export interface SlangNodeWrapper { + textRange: TextRange; + type: NodeType; + kind: NodeKind; + text: string; + pathRuleNodes: SlangNode[]; +} + +export function slangToVSCodeRange( + doc: TextDocument, + slangRange: TextRange +): Range { + return { + start: doc.positionAt(slangRange.start.utf16), + end: doc.positionAt(slangRange.end.utf16), + }; +} + +const SUPPORTED_PLATFORMS = [ + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "win32-arm64", + "win32-ia32", + "win32-x64", +]; + +export function isSlangSupported() { + const currentPlatform = getPlatform(); + + return SUPPORTED_PLATFORMS.includes(currentPlatform); +} + +export function getLanguage(versionPragmas: string[]): Language { + const supportedVersions = Language.supportedVersions(); + + const slangVersion = semver.maxSatisfying( + supportedVersions, + versionPragmas.join(" ") + ); + + if (slangVersion === null) { + throw new Error( + `No supported solidity version found. Supported versions: ${supportedVersions}, pragma directives: ${versionPragmas}` + ); + } + return new Language(slangVersion); +} diff --git a/server/src/server.ts b/server/src/server.ts index 8663d436..c2e78b46 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -19,6 +19,8 @@ import { Telemetry } from "./telemetry/types"; import { attachDocumentHooks } from "./services/documents/attachDocumentHooks"; import { availableVersions } from "./services/initialization/updateAvailableSolcVersions"; import { onDocumentFormatting } from "./services/formatting/onDocumentFormatting"; +import { onSemanticTokensFull } from "./services/semanticHighlight/onSemanticTokensFull"; +import { onDocumentSymbol } from "./services/documentSymbol/onDocumentSymbol"; export default function setupServer( connection: Connection, @@ -98,6 +100,8 @@ function attachLanguageServerCommandHooks(serverState: ServerState) { connection.onCodeAction(onCodeAction(serverState)); connection.onHover(onHover(serverState)); connection.onDocumentFormatting(onDocumentFormatting(serverState)); + connection.onDocumentSymbol(onDocumentSymbol(serverState)); + connection.languages.semanticTokens.on(onSemanticTokensFull(serverState)); } function attachCustomHooks(serverState: ServerState) { diff --git a/server/src/services/documentSymbol/SymbolTreeBuilder.ts b/server/src/services/documentSymbol/SymbolTreeBuilder.ts new file mode 100644 index 00000000..b777a87d --- /dev/null +++ b/server/src/services/documentSymbol/SymbolTreeBuilder.ts @@ -0,0 +1,50 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { DocumentSymbol } from "vscode-languageserver-types"; +import _ from "lodash"; + +export class SymbolTreeBuilder { + private symbols: DocumentSymbol[] = []; + private currentPath: Array> = []; + + public openSymbol(params: Partial) { + const symbol = { + children: [], + ...params, + }; + + this.currentPath.push(symbol); + } + + public closeSymbol() { + const symbol = this.currentPath.pop() as DocumentSymbol; + + if (symbol.name === undefined) { + return; + } + + if (this.currentPath.length === 0) { + this.symbols.push(symbol); + } else { + const lastSymbol = this.lastOpenSymbol(); + + if (!lastSymbol) { + throw new Error("Attempting to close a symbol but none is open"); + } + + lastSymbol.children!.push(symbol); + } + } + + public lastOpenSymbol() { + return _.last(this.currentPath); + } + + public getSymbols() { + // Close any left open symbols + while (this.lastOpenSymbol()) { + this.closeSymbol(); + } + + return this.symbols; + } +} diff --git a/server/src/services/documentSymbol/SymbolVisitor.ts b/server/src/services/documentSymbol/SymbolVisitor.ts new file mode 100644 index 00000000..2b8d1dff --- /dev/null +++ b/server/src/services/documentSymbol/SymbolVisitor.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { SymbolKind } from "vscode-languageserver-types"; +import _ from "lodash"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { TokenNode } from "@nomicfoundation/slang/cst"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import { Cursor } from "@nomicfoundation/slang/cursor"; +import { slangToVSCodeRange } from "../../parser/slangHelpers"; +import { SymbolTreeBuilder } from "./SymbolTreeBuilder"; + +export abstract class SymbolVisitor { + public abstract ruleKind: RuleKind; + public abstract symbolKind: SymbolKind; + public abstract nameTokenKind: TokenKind; + + constructor( + public document: TextDocument, + public symbolBuilder: SymbolTreeBuilder + ) {} + + public onRuleNode(cursor: Cursor): void { + const range = slangToVSCodeRange(this.document, cursor.textRange); + + let symbolName = "-"; + let selectionRange = slangToVSCodeRange(this.document, cursor.textRange); + + // Find identifier + const childCursor = cursor.spawn(); + + do { + const nameToken: TokenNode | null = childCursor.findTokenWithKind([ + this.nameTokenKind, + ]); + + if (nameToken && childCursor.pathRuleNodes.length === 1) { + symbolName = nameToken.text; + selectionRange = slangToVSCodeRange( + this.document, + childCursor.textRange + ); + break; + } + } while (childCursor.goToNext()); + + let lastOpenSymbol; + + while ((lastOpenSymbol = this.symbolBuilder.lastOpenSymbol())) { + const lastEndOffset = this.document.offsetAt(lastOpenSymbol.range!.end); + const currentEndOffset = this.document.offsetAt(range.end); + + if (lastEndOffset < currentEndOffset) { + this.symbolBuilder.closeSymbol(); + } else { + break; + } + } + + this.symbolBuilder.openSymbol({ + kind: this.symbolKind, + name: symbolName, + range, + selectionRange, + }); + } +} diff --git a/server/src/services/documentSymbol/onDocumentSymbol.ts b/server/src/services/documentSymbol/onDocumentSymbol.ts new file mode 100644 index 00000000..48b77321 --- /dev/null +++ b/server/src/services/documentSymbol/onDocumentSymbol.ts @@ -0,0 +1,127 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import { DocumentSymbolParams } from "vscode-languageserver/node"; +import { DocumentSymbol, SymbolInformation } from "vscode-languageserver-types"; +import { analyze } from "@nomicfoundation/solidity-analyzer"; +import _ from "lodash"; +import { ProductionKind } from "@nomicfoundation/slang/kinds"; +import { Cursor } from "@nomicfoundation/slang/cursor"; +import { RuleNode } from "@nomicfoundation/slang/cst"; +import { ServerState } from "../../types"; +import { getLanguage } from "../../parser/slangHelpers"; +import { SymbolTreeBuilder } from "./SymbolTreeBuilder"; +import { StructDefinition } from "./visitors/StructDefinition"; +import { StructMember } from "./visitors/StructMember"; +import { InterfaceDefinition } from "./visitors/InterfaceDefinition"; +import { FunctionDefinition } from "./visitors/FunctionDefinition"; +import { ContractDefinition } from "./visitors/ContractDefinition"; +import { EventDefinition } from "./visitors/EventDefinition"; +import { StateVariableDeclaration } from "./visitors/StateVariableDeclaration"; +import { VariableDeclaration } from "./visitors/VariableDeclaration"; +import { ConstantDefinition } from "./visitors/ConstantDefinition"; +import { ConstructorDefinition } from "./visitors/ConstructorDefinition"; +import { EnumDefinition } from "./visitors/EnumDefinition"; +import { ErrorDefinition } from "./visitors/ErrorDefinition"; +import { FallbackFunctionDefinition } from "./visitors/FallbackFunctionDefinition"; +import { LibraryDefinition } from "./visitors/LibraryDefinition"; +import { ModifierDefinition } from "./visitors/ModifierDefinition"; +import { ReceiveFunctionDefinition } from "./visitors/ReceiveFunctionDefinition"; +import { UserDefinedValueTypeDefinition } from "./visitors/UserDefinedValueTypeDefinition"; +import { SymbolVisitor } from "./SymbolVisitor"; +import { YulFunctionDefinition } from "./visitors/YulFunctionDefinition"; +import { UnnamedFunctionDefinition } from "./visitors/UnnamedFunctionDefinition"; + +export function onDocumentSymbol(serverState: ServerState) { + return async ( + params: DocumentSymbolParams + ): Promise => { + const { telemetry, logger } = serverState; + return telemetry.trackTimingSync("onDocumentSymbol", (transaction) => { + const { uri } = params.textDocument; + + // Find the file in the documents collection + const document = serverState.documents.get(uri); + + if (document === undefined) { + throw new Error(`${uri} not found in documents`); + } + + const text = document.getText(); + + // Get the document's solidity version + let span = transaction.startChild({ op: "solidity-analyzer" }); + const { versionPragmas } = analyze(text); + span.finish(); + + try { + const language = getLanguage(versionPragmas); + + // Parse using slang + span = transaction.startChild({ op: "slang-parsing" }); + + const parseOutput = language.parse( + ProductionKind.SourceUnit, + document.getText() + ); + + const parseTree = parseOutput.parseTree; + span.finish(); + + const builder = new SymbolTreeBuilder(); + + const visitors: SymbolVisitor[] = [ + new StructDefinition(document, builder), + new StructMember(document, builder), + new InterfaceDefinition(document, builder), + new FunctionDefinition(document, builder), + new ContractDefinition(document, builder), + new EventDefinition(document, builder), + new StateVariableDeclaration(document, builder), + new VariableDeclaration(document, builder), + new ConstantDefinition(document, builder), + new ConstructorDefinition(document, builder), + new EnumDefinition(document, builder), + new ErrorDefinition(document, builder), + new FallbackFunctionDefinition(document, builder), + new LibraryDefinition(document, builder), + new ModifierDefinition(document, builder), + new ReceiveFunctionDefinition(document, builder), + new UserDefinedValueTypeDefinition(document, builder), + new YulFunctionDefinition(document, builder), + new UnnamedFunctionDefinition(document, builder), + ]; + + const indexedVisitors = _.keyBy(visitors, "ruleKind"); + + const cursor: Cursor = parseTree.cursor; + const ruleKinds = visitors.map((v) => v.ruleKind); + let node: RuleNode; + + // Useful to keep this here for development + // const kursor: Cursor = parseTree.cursor.clone(); + // do { + // console.log( + // `${" ".repeat(kursor.pathRuleNodes.length)}${kursor.node.kind}(${ + // ["R", "T"][kursor.node.type] + // }): ${kursor.node?.text ?? ""}` + // ); + // } while (kursor.goToNext()); + + span = transaction.startChild({ op: "walk-generate-symbols" }); + while ((node = cursor.findRuleWithKind(ruleKinds)) !== null) { + const visitor: SymbolVisitor = indexedVisitors[node.kind]; + visitor.onRuleNode(cursor); + + cursor.goToNext(); + } + span.finish(); + + return { status: "ok", result: builder.getSymbols() }; + } catch (error) { + logger.error(`Document Symbol Error: ${error}`); + return { status: "internal_error", result: null }; + } + }); + }; +} diff --git a/server/src/services/documentSymbol/visitors/ConstantDefinition.ts b/server/src/services/documentSymbol/visitors/ConstantDefinition.ts new file mode 100644 index 00000000..a83013d5 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/ConstantDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class ConstantDefinition extends SymbolVisitor { + public ruleKind = RuleKind.ConstantDefinition; + public symbolKind = SymbolKind.Constant; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/ConstructorDefinition.ts b/server/src/services/documentSymbol/visitors/ConstructorDefinition.ts new file mode 100644 index 00000000..c1faaee9 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/ConstructorDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class ConstructorDefinition extends SymbolVisitor { + public ruleKind = RuleKind.ConstructorDefinition; + public symbolKind = SymbolKind.Constructor; + public nameTokenKind = TokenKind.ConstructorKeyword; +} diff --git a/server/src/services/documentSymbol/visitors/ContractDefinition.ts b/server/src/services/documentSymbol/visitors/ContractDefinition.ts new file mode 100644 index 00000000..e5f0a9d3 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/ContractDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class ContractDefinition extends SymbolVisitor { + public ruleKind = RuleKind.ContractDefinition; + public symbolKind = SymbolKind.Class; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/EnumDefinition.ts b/server/src/services/documentSymbol/visitors/EnumDefinition.ts new file mode 100644 index 00000000..e27f715f --- /dev/null +++ b/server/src/services/documentSymbol/visitors/EnumDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class EnumDefinition extends SymbolVisitor { + public ruleKind = RuleKind.EnumDefinition; + public symbolKind = SymbolKind.Enum; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/ErrorDefinition.ts b/server/src/services/documentSymbol/visitors/ErrorDefinition.ts new file mode 100644 index 00000000..7f607faa --- /dev/null +++ b/server/src/services/documentSymbol/visitors/ErrorDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class ErrorDefinition extends SymbolVisitor { + public ruleKind = RuleKind.ErrorDefinition; + public symbolKind = SymbolKind.Event; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/EventDefinition.ts b/server/src/services/documentSymbol/visitors/EventDefinition.ts new file mode 100644 index 00000000..32bb90eb --- /dev/null +++ b/server/src/services/documentSymbol/visitors/EventDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class EventDefinition extends SymbolVisitor { + public ruleKind = RuleKind.EventDefinition; + public symbolKind = SymbolKind.Event; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/FallbackFunctionDefinition.ts b/server/src/services/documentSymbol/visitors/FallbackFunctionDefinition.ts new file mode 100644 index 00000000..379b94ce --- /dev/null +++ b/server/src/services/documentSymbol/visitors/FallbackFunctionDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class FallbackFunctionDefinition extends SymbolVisitor { + public ruleKind = RuleKind.FallbackFunctionDefinition; + public symbolKind = SymbolKind.Function; + public nameTokenKind = TokenKind.FallbackKeyword; +} diff --git a/server/src/services/documentSymbol/visitors/FunctionDefinition.ts b/server/src/services/documentSymbol/visitors/FunctionDefinition.ts new file mode 100644 index 00000000..4f336bec --- /dev/null +++ b/server/src/services/documentSymbol/visitors/FunctionDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class FunctionDefinition extends SymbolVisitor { + public ruleKind = RuleKind.FunctionDefinition; + public symbolKind = SymbolKind.Function; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/InterfaceDefinition.ts b/server/src/services/documentSymbol/visitors/InterfaceDefinition.ts new file mode 100644 index 00000000..7bf9ae13 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/InterfaceDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class InterfaceDefinition extends SymbolVisitor { + public ruleKind = RuleKind.InterfaceDefinition; + public symbolKind = SymbolKind.Interface; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/LibraryDefinition.ts b/server/src/services/documentSymbol/visitors/LibraryDefinition.ts new file mode 100644 index 00000000..2d54d91d --- /dev/null +++ b/server/src/services/documentSymbol/visitors/LibraryDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class LibraryDefinition extends SymbolVisitor { + public ruleKind = RuleKind.LibraryDefinition; + public symbolKind = SymbolKind.Class; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/ModifierDefinition.ts b/server/src/services/documentSymbol/visitors/ModifierDefinition.ts new file mode 100644 index 00000000..0456ab4b --- /dev/null +++ b/server/src/services/documentSymbol/visitors/ModifierDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class ModifierDefinition extends SymbolVisitor { + public ruleKind = RuleKind.ModifierDefinition; + public symbolKind = SymbolKind.Function; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/ReceiveFunctionDefinition.ts b/server/src/services/documentSymbol/visitors/ReceiveFunctionDefinition.ts new file mode 100644 index 00000000..8927c6b0 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/ReceiveFunctionDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class ReceiveFunctionDefinition extends SymbolVisitor { + public ruleKind = RuleKind.ReceiveFunctionDefinition; + public symbolKind = SymbolKind.Function; + public nameTokenKind = TokenKind.ReceiveKeyword; +} diff --git a/server/src/services/documentSymbol/visitors/StateVariableDeclaration.ts b/server/src/services/documentSymbol/visitors/StateVariableDeclaration.ts new file mode 100644 index 00000000..7f6b17e9 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/StateVariableDeclaration.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class StateVariableDeclaration extends SymbolVisitor { + public ruleKind = RuleKind.StateVariableDefinition; + public symbolKind = SymbolKind.Property; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/StructDefinition.ts b/server/src/services/documentSymbol/visitors/StructDefinition.ts new file mode 100644 index 00000000..5f9b69a5 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/StructDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class StructDefinition extends SymbolVisitor { + public ruleKind = RuleKind.StructDefinition; + public symbolKind = SymbolKind.Struct; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/StructMember.ts b/server/src/services/documentSymbol/visitors/StructMember.ts new file mode 100644 index 00000000..b3f6e734 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/StructMember.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class StructMember extends SymbolVisitor { + public ruleKind = RuleKind.StructMember; + public symbolKind = SymbolKind.Property; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/UnnamedFunctionDefinition.ts b/server/src/services/documentSymbol/visitors/UnnamedFunctionDefinition.ts new file mode 100644 index 00000000..54bdcfc5 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/UnnamedFunctionDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class UnnamedFunctionDefinition extends SymbolVisitor { + public ruleKind = RuleKind.UnnamedFunctionDefinition; + public symbolKind = SymbolKind.Function; + public nameTokenKind = TokenKind.FunctionKeyword; +} diff --git a/server/src/services/documentSymbol/visitors/UserDefinedValueTypeDefinition.ts b/server/src/services/documentSymbol/visitors/UserDefinedValueTypeDefinition.ts new file mode 100644 index 00000000..7067bd18 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/UserDefinedValueTypeDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class UserDefinedValueTypeDefinition extends SymbolVisitor { + public ruleKind = RuleKind.UserDefinedValueTypeDefinition; + public symbolKind = SymbolKind.TypeParameter; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/VariableDeclaration.ts b/server/src/services/documentSymbol/visitors/VariableDeclaration.ts new file mode 100644 index 00000000..ad8af4b4 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/VariableDeclaration.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class VariableDeclaration extends SymbolVisitor { + public ruleKind = RuleKind.VariableDeclaration; + public symbolKind = SymbolKind.Variable; + public nameTokenKind = TokenKind.Identifier; +} diff --git a/server/src/services/documentSymbol/visitors/YulFunctionDefinition.ts b/server/src/services/documentSymbol/visitors/YulFunctionDefinition.ts new file mode 100644 index 00000000..e1565c83 --- /dev/null +++ b/server/src/services/documentSymbol/visitors/YulFunctionDefinition.ts @@ -0,0 +1,9 @@ +import { SymbolKind } from "vscode-languageserver-types"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { SymbolVisitor } from "../SymbolVisitor"; + +export class YulFunctionDefinition extends SymbolVisitor { + public ruleKind = RuleKind.YulFunctionDefinition; + public symbolKind = SymbolKind.Function; + public nameTokenKind = TokenKind.YulIdentifier; +} diff --git a/server/src/services/initialization/featureFlags.ts b/server/src/services/initialization/featureFlags.ts new file mode 100644 index 00000000..0058f801 --- /dev/null +++ b/server/src/services/initialization/featureFlags.ts @@ -0,0 +1,76 @@ +import got from "got"; +import crypto from "crypto"; +import { ServerState } from "../../types"; +import { isTestMode } from "../../utils"; + +export interface FeatureFlag { + percent: number; +} + +export interface FeatureFlags { + semanticHighlighting: FeatureFlag; + documentSymbol: FeatureFlag; +} + +const DEFAULT_FLAGS: FeatureFlags = { + semanticHighlighting: { + percent: 0, + }, + documentSymbol: { + percent: 0, + }, +}; + +export async function fetchFeatureFlags( + state: ServerState +): Promise { + state.logger.info("Fetching feature flags"); + + try { + return await got + .get( + "https://raw.githubusercontent.com/NomicFoundation/hardhat-vscode/development/flags.json", + { + timeout: 2000, + } + ) + .json(); + } catch (error) { + state.telemetry.captureException(error); + return DEFAULT_FLAGS; + } +} + +export function isFeatureEnabled( + { logger }: ServerState, + flags: FeatureFlags, + feature: keyof FeatureFlags, + machineId?: string +): boolean { + const flag = flags[feature]; + + if (machineId === undefined) { + logger.info(`MachineId is undefined, turning feature flags off`); + return false; + } + + if (flag === undefined) { + throw new Error(`Feature flag not found: ${feature}`); + } + + if (isTestMode()) { + return true; + } + + // hash the machineId to get an evenly distributed value + const machineIdHash = crypto.createHash("md5"); + machineIdHash.update(machineId); + const digest = machineIdHash.digest("hex").slice(-4); // get last 2 bytes + + // check what percentile the current machineId is in + const numberDigest = parseInt(digest, 16); + const percentile = numberDigest / 65536; + const enabled = percentile < flag.percent; + + return enabled; +} diff --git a/server/src/services/initialization/onInitialize.ts b/server/src/services/initialization/onInitialize.ts index 3a194af9..cba45bcc 100644 --- a/server/src/services/initialization/onInitialize.ts +++ b/server/src/services/initialization/onInitialize.ts @@ -5,8 +5,12 @@ import { WorkspaceFolder, } from "vscode-languageserver/node"; import { ServerState } from "../../types"; +import { tokensTypes } from "../semanticHighlight/tokenTypes"; +import { isSlangSupported } from "../../parser/slangHelpers"; +import { getPlatform } from "../../utils/operatingSystem"; import { indexWorkspaceFolders } from "./indexWorkspaceFolders"; import { updateAvailableSolcVersions } from "./updateAvailableSolcVersions"; +import { fetchFeatureFlags, isFeatureEnabled } from "./featureFlags"; export const onInitialize = (serverState: ServerState) => { const { logger } = serverState; @@ -40,22 +44,49 @@ export const onInitialize = (serverState: ServerState) => { workspaceFolders, }); - // fetch available solidity versions - await updateAvailableSolcVersions(serverState); + // fetch available solidity versions and feature flags + const [flags, _] = await Promise.all([ + fetchFeatureFlags(serverState), + updateAvailableSolcVersions(serverState), + ]); logger.info("Language server ready"); + const slangSupported = isSlangSupported(); + + const semanticTokensEnabled = isFeatureEnabled( + serverState, + flags, + "semanticHighlighting", + machineId + ); + + const documentSymbolsEnabled = isFeatureEnabled( + serverState, + flags, + "documentSymbol", + machineId + ); // Index and analysis - await serverState.telemetry.trackTiming("indexing", async (transaction) => { - await indexWorkspaceFolders( - serverState, - serverState.workspaceFileRetriever, - workspaceFolders, - transaction - ); - - return { status: "ok", result: null }; - }); + await serverState.telemetry.trackTiming( + "indexing", + async (transaction) => { + await indexWorkspaceFolders( + serverState, + serverState.workspaceFileRetriever, + workspaceFolders, + transaction + ); + + return { status: "ok", result: null }; + }, + { + platform: getPlatform(), + slangSupported, + semanticTokensEnabled, + documentSymbolsEnabled, + } + ); // Build and return InitializeResult const result: InitializeResult = { @@ -79,7 +110,15 @@ export const onInitialize = (serverState: ServerState) => { codeActionProvider: true, hoverProvider: true, documentFormattingProvider: true, - + semanticTokensProvider: { + legend: { + tokenTypes: tokensTypes, + tokenModifiers: [], + }, + range: false, + full: slangSupported && semanticTokensEnabled, + }, + documentSymbolProvider: slangSupported && documentSymbolsEnabled, workspace: { workspaceFolders: { supported: false, diff --git a/server/src/services/semanticHighlight/HighlightVisitor.ts b/server/src/services/semanticHighlight/HighlightVisitor.ts new file mode 100644 index 00000000..14a6e91a --- /dev/null +++ b/server/src/services/semanticHighlight/HighlightVisitor.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { TextDocument } from "vscode-languageserver-textdocument"; +import { TokenKind } from "@nomicfoundation/slang/kinds"; +import { SlangNodeWrapper } from "../../parser/slangHelpers"; +import { SemanticTokensBuilder } from "./SemanticTokensBuilder"; + +// Abstraction for a visitor that wants to highlight tokens +export abstract class HighlightVisitor { + public abstract tokenKinds: Set; + + constructor( + public document: TextDocument, + public tokenBuilder: SemanticTokensBuilder + ) {} + + public enter(nodeWrapper: SlangNodeWrapper): void {} +} diff --git a/server/src/services/semanticHighlight/SemanticTokensBuilder.ts b/server/src/services/semanticHighlight/SemanticTokensBuilder.ts new file mode 100644 index 00000000..166577fa --- /dev/null +++ b/server/src/services/semanticHighlight/SemanticTokensBuilder.ts @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import { SlangNodeWrapper } from "../../parser/slangHelpers"; +import { getTokenTypeIndex } from "./tokenTypes"; + +// Helps building a SemanticTokens response by providing slang nodes and supported token types +export class SemanticTokensBuilder { + private tokenData: number[] = []; + private lastTokenLine = 0; + private lastTokenChar = 0; + + constructor(private document: TextDocument) {} + + public addToken( + nodeWrapper: SlangNodeWrapper, + type: SemanticTokenTypes, + modifiers = 0 + ) { + const offset = nodeWrapper.textRange.start.utf16; + const length = + nodeWrapper.textRange.end.utf16 - nodeWrapper.textRange.start.utf16; + + const position = this.document.positionAt(offset); + + // Calculate character and line difference to last token + const lineDelta = position.line - this.lastTokenLine; + const charDelta = + lineDelta === 0 + ? position.character - this.lastTokenChar + : position.character; + + this.lastTokenLine = position.line; + this.lastTokenChar = position.character; + + this.tokenData.push( + lineDelta, + charDelta, + length, + getTokenTypeIndex(type), + modifiers + ); + } + + public getTokenData() { + return this.tokenData; + } +} diff --git a/server/src/services/semanticHighlight/highlighters/ContractDefinitionHighlighter.ts b/server/src/services/semanticHighlight/highlighters/ContractDefinitionHighlighter.ts new file mode 100644 index 00000000..a0449c91 --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/ContractDefinitionHighlighter.ts @@ -0,0 +1,21 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +// Highlights contract definitions +export class ContractDefinitionHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 1]?.kind === RuleKind.ContractDefinition + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.type); + } + } +} diff --git a/server/src/services/semanticHighlight/highlighters/CustomTypeHighlighter.ts b/server/src/services/semanticHighlight/highlighters/CustomTypeHighlighter.ts new file mode 100644 index 00000000..7336ec19 --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/CustomTypeHighlighter.ts @@ -0,0 +1,21 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +// Highlights custom type names +export class CustomTypeHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 2]?.kind === RuleKind.TypeName + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.type); + } + } +} diff --git a/server/src/services/semanticHighlight/highlighters/EnumDefinitionHighlighter.ts b/server/src/services/semanticHighlight/highlighters/EnumDefinitionHighlighter.ts new file mode 100644 index 00000000..ff239114 --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/EnumDefinitionHighlighter.ts @@ -0,0 +1,20 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +export class EnumDefinitionHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 1]?.kind === RuleKind.EnumDefinition + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.type); + } + } +} diff --git a/server/src/services/semanticHighlight/highlighters/ErrorDefinitionHighlighter.ts b/server/src/services/semanticHighlight/highlighters/ErrorDefinitionHighlighter.ts new file mode 100644 index 00000000..76b46d8c --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/ErrorDefinitionHighlighter.ts @@ -0,0 +1,20 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +export class ErrorDefinitionHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 1]?.kind === RuleKind.ErrorDefinition + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.type); + } + } +} diff --git a/server/src/services/semanticHighlight/highlighters/EventDefinitionHighlighter.ts b/server/src/services/semanticHighlight/highlighters/EventDefinitionHighlighter.ts new file mode 100644 index 00000000..beeda201 --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/EventDefinitionHighlighter.ts @@ -0,0 +1,21 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +// Highlights event definitions +export class EventDefinitionHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 1]?.kind === RuleKind.EventDefinition + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.type); + } + } +} diff --git a/server/src/services/semanticHighlight/highlighters/EventEmissionHighlighter.ts b/server/src/services/semanticHighlight/highlighters/EventEmissionHighlighter.ts new file mode 100644 index 00000000..96bc1611 --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/EventEmissionHighlighter.ts @@ -0,0 +1,21 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +// Highlights event emissions +export class EventEmissionHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 2]?.kind === RuleKind.EmitStatement + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.type); + } + } +} diff --git a/server/src/services/semanticHighlight/highlighters/FunctionCallHighlighter.ts b/server/src/services/semanticHighlight/highlighters/FunctionCallHighlighter.ts new file mode 100644 index 00000000..52b8b7f9 --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/FunctionCallHighlighter.ts @@ -0,0 +1,21 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +// Highlights function calls +export class FunctionCallHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 2]?.kind === RuleKind.FunctionCallExpression + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.function); + } + } +} diff --git a/server/src/services/semanticHighlight/highlighters/FunctionDefinitionHighlighter.ts b/server/src/services/semanticHighlight/highlighters/FunctionDefinitionHighlighter.ts new file mode 100644 index 00000000..bce240a5 --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/FunctionDefinitionHighlighter.ts @@ -0,0 +1,21 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +// Highlights function definitions +export class FunctionDefinitionHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 1]?.kind === RuleKind.FunctionDefinition + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.function); + } + } +} diff --git a/server/src/services/semanticHighlight/highlighters/InterfaceDefinitionHighlighter.ts b/server/src/services/semanticHighlight/highlighters/InterfaceDefinitionHighlighter.ts new file mode 100644 index 00000000..3c2e12d8 --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/InterfaceDefinitionHighlighter.ts @@ -0,0 +1,21 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +// Highlights interface definitions +export class InterfaceDefinitionHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 1]?.kind === RuleKind.InterfaceDefinition + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.type); + } + } +} diff --git a/server/src/services/semanticHighlight/highlighters/LibraryDefinitionHighlighter.ts b/server/src/services/semanticHighlight/highlighters/LibraryDefinitionHighlighter.ts new file mode 100644 index 00000000..5cc2ec6c --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/LibraryDefinitionHighlighter.ts @@ -0,0 +1,20 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +export class LibraryDefinitionHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 1]?.kind === RuleKind.LibraryDefinition + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.type); + } + } +} diff --git a/server/src/services/semanticHighlight/highlighters/StructDefinitionHighlighter.ts b/server/src/services/semanticHighlight/highlighters/StructDefinitionHighlighter.ts new file mode 100644 index 00000000..1a31047a --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/StructDefinitionHighlighter.ts @@ -0,0 +1,21 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +// Highlights struct definitions +export class StructDefinitionHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 1]?.kind === RuleKind.StructDefinition + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.type); + } + } +} diff --git a/server/src/services/semanticHighlight/highlighters/UserDefinedValueTypeDefinitionHighlighter copy.ts b/server/src/services/semanticHighlight/highlighters/UserDefinedValueTypeDefinitionHighlighter copy.ts new file mode 100644 index 00000000..96a6f7bf --- /dev/null +++ b/server/src/services/semanticHighlight/highlighters/UserDefinedValueTypeDefinitionHighlighter copy.ts @@ -0,0 +1,21 @@ +import { SemanticTokenTypes } from "vscode-languageserver-protocol"; +import { NodeType } from "@nomicfoundation/slang/cst"; +import { RuleKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { HighlightVisitor } from "../HighlightVisitor"; +import { SlangNodeWrapper } from "../../../parser/slangHelpers"; + +export class UserDefinedValueTypeDefinitionHighlighter extends HighlightVisitor { + public tokenKinds = new Set([TokenKind.Identifier]); + + public enter(nodeWrapper: SlangNodeWrapper): void { + const ancestors = nodeWrapper.pathRuleNodes; + if ( + nodeWrapper.type === NodeType.Token && + nodeWrapper.kind === TokenKind.Identifier && + ancestors[ancestors.length - 1]?.kind === + RuleKind.UserDefinedValueTypeDefinition + ) { + this.tokenBuilder.addToken(nodeWrapper, SemanticTokenTypes.type); + } + } +} diff --git a/server/src/services/semanticHighlight/onSemanticTokensFull.ts b/server/src/services/semanticHighlight/onSemanticTokensFull.ts new file mode 100644 index 00000000..f997bf2e --- /dev/null +++ b/server/src/services/semanticHighlight/onSemanticTokensFull.ts @@ -0,0 +1,137 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import { + SemanticTokens, + SemanticTokensParams, +} from "vscode-languageserver-protocol"; +import _, { Dictionary } from "lodash"; +import { analyze } from "@nomicfoundation/solidity-analyzer"; +import { ProductionKind, TokenKind } from "@nomicfoundation/slang/kinds"; +import { Cursor } from "@nomicfoundation/slang/cursor"; +import { TokenNode } from "@nomicfoundation/slang/cst"; +import { ServerState } from "../../types"; +import { getLanguage } from "../../parser/slangHelpers"; +import { CustomTypeHighlighter } from "./highlighters/CustomTypeHighlighter"; +import { SemanticTokensBuilder } from "./SemanticTokensBuilder"; +import { FunctionDefinitionHighlighter } from "./highlighters/FunctionDefinitionHighlighter"; +import { FunctionCallHighlighter } from "./highlighters/FunctionCallHighlighter"; +import { EventEmissionHighlighter } from "./highlighters/EventEmissionHighlighter"; +import { EventDefinitionHighlighter } from "./highlighters/EventDefinitionHighlighter"; +import { ContractDefinitionHighlighter } from "./highlighters/ContractDefinitionHighlighter"; +import { InterfaceDefinitionHighlighter } from "./highlighters/InterfaceDefinitionHighlighter"; +import { StructDefinitionHighlighter } from "./highlighters/StructDefinitionHighlighter"; +import { HighlightVisitor } from "./HighlightVisitor"; +import { UserDefinedValueTypeDefinitionHighlighter } from "./highlighters/UserDefinedValueTypeDefinitionHighlighter copy"; +import { EnumDefinitionHighlighter } from "./highlighters/EnumDefinitionHighlighter"; +import { ErrorDefinitionHighlighter } from "./highlighters/ErrorDefinitionHighlighter"; +import { LibraryDefinitionHighlighter } from "./highlighters/LibraryDefinitionHighlighter"; + +const emptyResponse: SemanticTokens = { data: [] }; + +export function onSemanticTokensFull(serverState: ServerState) { + return (params: SemanticTokensParams): SemanticTokens => { + const { telemetry, logger } = serverState; + + return ( + telemetry.trackTimingSync("onSemanticTokensFull", (transaction) => { + const { uri } = params.textDocument; + + // Find the file in the documents collection + const document = serverState.documents.get(uri); + + if (document === undefined) { + logger.error("document not found in collection"); + return { + status: "internal_error", + result: emptyResponse, + }; + } + + const text = document.getText(); + + // Get the document's solidity version + let span = transaction.startChild({ op: "solidity-analyzer" }); + const { versionPragmas } = analyze(text); + span.finish(); + + const language = getLanguage(versionPragmas); + + try { + // Parse using slang + span = transaction.startChild({ op: "slang-parsing" }); + + const parseOutput = language.parse( + ProductionKind.SourceUnit, + document.getText() + ); + + const parseTree = parseOutput.parseTree; + span.finish(); + + // Register visitors + const builder = new SemanticTokensBuilder(document); + + const visitors = [ + new CustomTypeHighlighter(document, builder), + new FunctionDefinitionHighlighter(document, builder), + new FunctionCallHighlighter(document, builder), + new EventEmissionHighlighter(document, builder), + new EventDefinitionHighlighter(document, builder), + new ContractDefinitionHighlighter(document, builder), + new InterfaceDefinitionHighlighter(document, builder), + new StructDefinitionHighlighter(document, builder), + new UserDefinedValueTypeDefinitionHighlighter(document, builder), + new EnumDefinitionHighlighter(document, builder), + new ErrorDefinitionHighlighter(document, builder), + new LibraryDefinitionHighlighter(document, builder), + ]; + + // Visit the CST + const indexedVisitors: Dictionary = {}; + const registeredTokenKinds: TokenKind[] = []; + + for (const visitor of visitors) { + for (const tokenKind of visitor.tokenKinds) { + indexedVisitors[tokenKind] ||= []; + indexedVisitors[tokenKind].push(visitor); + + if (!registeredTokenKinds.includes(tokenKind)) { + registeredTokenKinds.push(tokenKind); + } + } + } + + const cursor: Cursor = parseTree.cursor; + let node: TokenNode; + + span = transaction.startChild({ op: "walk-highlight-tokens" }); + while ( + (node = cursor.findTokenWithKind(registeredTokenKinds)) !== null + ) { + const nodeWrapper = { + kind: node.kind, + pathRuleNodes: cursor.pathRuleNodes, + text: node.text, + textRange: cursor.textRange, + type: node.type, + }; + + const registeredVisitors = indexedVisitors[nodeWrapper.kind]; + for (const visitor of registeredVisitors) { + visitor.enter(nodeWrapper); + } + + cursor.goToNext(); + } + span.finish(); + + return { status: "ok", result: { data: builder.getTokenData() } }; + } catch (error) { + logger.error(`Semantic Highlighting Error: ${error}`); + return { status: "internal_error", result: emptyResponse }; + } + }) || emptyResponse + ); + }; +} diff --git a/server/src/services/semanticHighlight/tokenTypes.ts b/server/src/services/semanticHighlight/tokenTypes.ts new file mode 100644 index 00000000..23c8b18f --- /dev/null +++ b/server/src/services/semanticHighlight/tokenTypes.ts @@ -0,0 +1,29 @@ +import { SemanticTokenTypes } from "vscode-languageserver-types"; + +// Semantic token types that we support +export const tokensTypes = [ + SemanticTokenTypes.keyword, + SemanticTokenTypes.number, + SemanticTokenTypes.type, + SemanticTokenTypes.string, + SemanticTokenTypes.function, +]; + +const tokenTypesMap = tokensTypes.reduce( + (map: Record, tokenType, index) => { + map[tokenType] = index; + return map; + }, + {} +); + +// Helper function to get the index of our supported token types list +export function getTokenTypeIndex(token: string) { + const tokenType = tokenTypesMap[token]; + + if (tokenType === undefined) { + throw new Error(`Invalid token type requested: ${token}`); + } + + return tokenType; +} diff --git a/server/src/utils/onCommand.ts b/server/src/utils/onCommand.ts index 33d56b2a..afca0984 100644 --- a/server/src/utils/onCommand.ts +++ b/server/src/utils/onCommand.ts @@ -1,5 +1,6 @@ import { ISolFileEntry } from "@common/types"; import { TextDocument } from "vscode-languageserver-textdocument"; +import { Transaction } from "@sentry/types"; import { addFrameworkTag } from "../telemetry/tags"; import { ServerState } from "../types"; import { lookupEntryForUri } from "./lookupEntryForUri"; @@ -8,7 +9,11 @@ export function onCommand( serverState: ServerState, commandName: string, uri: string, - action: (documentAnalyzer: ISolFileEntry, document: TextDocument) => T + action: ( + documentAnalyzer: ISolFileEntry, + document: TextDocument, + transaction: Transaction + ) => T ) { const { logger, telemetry } = serverState; @@ -28,6 +33,9 @@ export function onCommand( addFrameworkTag(transaction, documentAnalyzer.project); - return { status: "ok", result: action(documentAnalyzer, document) }; + return { + status: "ok", + result: action(documentAnalyzer, document, transaction), + }; }); } diff --git a/server/src/utils/operatingSystem.ts b/server/src/utils/operatingSystem.ts index 0537cc3b..a62ccdc4 100644 --- a/server/src/utils/operatingSystem.ts +++ b/server/src/utils/operatingSystem.ts @@ -16,3 +16,7 @@ export async function runCmd(cmd: string, cwd?: string): Promise { }); }); } + +export function getPlatform() { + return `${os.platform()}-${os.arch()}`; +} diff --git a/server/test/helpers/setupMockConnection.ts b/server/test/helpers/setupMockConnection.ts index 7731f2ff..767abde2 100644 --- a/server/test/helpers/setupMockConnection.ts +++ b/server/test/helpers/setupMockConnection.ts @@ -26,6 +26,12 @@ export function setupMockConnection() { sendNotification: sinon.spy(), onCodeAction: sinon.spy(), onDocumentFormatting: sinon.spy(), + onDocumentSymbol: sinon.spy(), + languages: { + semanticTokens: { + on: sinon.spy(), + }, + }, onNotification: sinon.fake( ( _method: string, diff --git a/test/protocol/projects/hardhat/contracts/documentSymbol/DocumentSymbols.sol b/test/protocol/projects/hardhat/contracts/documentSymbol/DocumentSymbols.sol new file mode 100644 index 00000000..97cd7179 --- /dev/null +++ b/test/protocol/projects/hardhat/contracts/documentSymbol/DocumentSymbols.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.8; + +type CustomType is uint256; + +interface TestInterface { + function interfaceFunction(uint256 param) external returns (string memory); +} + +struct TestStruct { + uint256 aNumber; + string aString; + address anAddress; +} + +struct TestStruct2 { + uint256 aNumber; + string aString; + address anAddress; +} + +uint256 constant aConstant = 1234; + +enum Name { + A, + B +} + +error CustomError(); + +library TestLibrary {} + +contract testContract { + constructor() { + uint256 local = aConstant; + local; + } + + modifier testModifier() { + _; + } + + event TestEvent( + testContract contractAsEventParam, TestInterface interfaceAsEventParam, TestStruct structAsEventParam + ); + + testContract contractAsMember; + + TestInterface interfaceAsMember; + + TestStruct structAsMember; + + uint256 aNumber = 0x12 + 123; + + string aString = "asdf"; + + function testFunction( + testContract contractAsFuncParam, + TestInterface interfaceAsFuncParam, + TestStruct memory structAsFuncParam + ) public { + testContract contractAsLocalVar; + + TestInterface interfaceAsLocalVar; + TestStruct memory structAsLocalVar; + contractAsLocalVar; + contractAsFuncParam; + interfaceAsFuncParam; + interfaceAsLocalVar; + structAsFuncParam; + structAsLocalVar; + + emit TestEvent(contractAsLocalVar, interfaceAsLocalVar, structAsLocalVar); + + //ñññññ + + TestStruct memory afterUTF8; + afterUTF8; + anotherFunction(); + + assembly { + function mult(a, b) -> result { + result := mul(a, b) + } + } + } + + function anotherFunction() public pure {} + + fallback() external {} + + receive() external payable {} +} diff --git a/test/protocol/projects/hardhat/contracts/semanticTokens/full/SemanticTokens.sol b/test/protocol/projects/hardhat/contracts/semanticTokens/full/SemanticTokens.sol new file mode 100644 index 00000000..fee819fd --- /dev/null +++ b/test/protocol/projects/hardhat/contracts/semanticTokens/full/SemanticTokens.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.8; + +type CustomType is uint256; + +interface TestInterface {} + +struct TestStruct { + uint256 aNumber; + string aString; + address anAddress; +} + +enum TestEnum { + A, + B, + C +} + +error CustomError(); + +library MyLibrary {} + +contract testContract { + event TestEvent( + testContract contractAsEventParam, TestInterface interfaceAsEventParam, TestStruct structAsEventParam + ); + + testContract contractAsMember; + + TestInterface interfaceAsMember; + + TestStruct structAsMember; + + uint256 aNumber = 0x12 + 123; + + string aString = "asdf"; + + function testFunction( + testContract contractAsFuncParam, + TestInterface interfaceAsFuncParam, + TestStruct memory structAsFuncParam + ) public { + testContract contractAsLocalVar; + + TestInterface interfaceAsLocalVar; + TestStruct memory structAsLocalVar; + + // The following expression statements should not be highlighted + contractAsLocalVar; + contractAsFuncParam; + interfaceAsFuncParam; + interfaceAsLocalVar; + structAsFuncParam; + structAsLocalVar; + + emit TestEvent(contractAsLocalVar, interfaceAsLocalVar, structAsLocalVar); + + //ñññññ + + TestStruct memory afterUTF8; + afterUTF8; + anotherFunction(); + } + + function anotherFunction() public pure {} +} diff --git a/test/protocol/projects/projectless/src/documentSymbol/UnnamedFunction.sol b/test/protocol/projects/projectless/src/documentSymbol/UnnamedFunction.sol new file mode 100644 index 00000000..ab9e6fd1 --- /dev/null +++ b/test/protocol/projects/projectless/src/documentSymbol/UnnamedFunction.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.4.11; + +contract UnnamedTest { + function() payable {} +} diff --git a/test/protocol/src/TestLanguageClient.ts b/test/protocol/src/TestLanguageClient.ts index 37bacf66..15df0780 100644 --- a/test/protocol/src/TestLanguageClient.ts +++ b/test/protocol/src/TestLanguageClient.ts @@ -21,6 +21,8 @@ import { DidOpenTextDocumentParams, DocumentFormattingParams, DocumentFormattingRequest, + DocumentSymbolParams, + DocumentSymbolRequest, ImplementationParams, ImplementationRequest, InitializedNotification, @@ -34,6 +36,8 @@ import { ReferencesRequest, RenameParams, RenameRequest, + SemanticTokensParams, + SemanticTokensRequest, TypeDefinitionParams, TypeDefinitionRequest, } from 'vscode-languageserver-protocol/node' @@ -342,6 +346,26 @@ export class TestLanguageClient { return this.connection!.sendRequest(RenameRequest.type, params) } + public async getSemanticTokensFull(uri: string) { + const params: SemanticTokensParams = { + textDocument: { + uri, + }, + } + + return this.connection!.sendRequest(SemanticTokensRequest.type, params) + } + + public async getDocumentSymbols(uri: string) { + const params: DocumentSymbolParams = { + textDocument: { + uri, + }, + } + + return this.connection!.sendRequest(DocumentSymbolRequest.type, params) + } + public async formatDocument(uri: string) { const params: DocumentFormattingParams = { textDocument: { diff --git a/test/protocol/test/initialize/data/initializeResult.json b/test/protocol/test/initialize/data/initializeResult.json index 4d37991d..aaa1cf6c 100644 --- a/test/protocol/test/initialize/data/initializeResult.json +++ b/test/protocol/test/initialize/data/initializeResult.json @@ -18,6 +18,15 @@ "renameProvider": true, "codeActionProvider": true, "hoverProvider": true, + "documentSymbolProvider": true, + "semanticTokensProvider": { + "full": true, + "legend": { + "tokenModifiers": [], + "tokenTypes": ["keyword", "number", "type", "string", "function"] + }, + "range": false + }, "workspace": { "workspaceFolders": { "supported": true, diff --git a/test/protocol/test/textDocument/documentSymbol/documentSymbol.test.ts b/test/protocol/test/textDocument/documentSymbol/documentSymbol.test.ts new file mode 100644 index 00000000..14216fca --- /dev/null +++ b/test/protocol/test/textDocument/documentSymbol/documentSymbol.test.ts @@ -0,0 +1,961 @@ +import { expect } from 'chai' +import { test } from 'mocha' +import { TestLanguageClient } from '../../../src/TestLanguageClient' +import { getInitializedClient } from '../../client' +import { getProjectPath } from '../../helpers' +import { toUri } from '../../../src/helpers' + +let client!: TestLanguageClient + +describe('[hardhat] documentSymbol', () => { + let testPath: string + + before(async () => { + client = await getInitializedClient() + + testPath = getProjectPath('hardhat/contracts/documentSymbol/DocumentSymbols.sol') + + await client.openDocument(testPath) + }) + + after(async () => { + client.closeAllDocuments() + }) + + test('provides all the document symbols', async function () { + const symbols = await client.getDocumentSymbols(toUri(testPath)) + + expect(symbols).to.deep.equal([ + { + children: [], + kind: 26, + name: 'CustomType', + range: { + start: { + line: 3, + character: 0, + }, + end: { + line: 5, + character: 0, + }, + }, + selectionRange: { + start: { + line: 4, + character: 5, + }, + end: { + line: 4, + character: 15, + }, + }, + }, + { + children: [ + { + children: [], + kind: 12, + name: 'interfaceFunction', + range: { + start: { + line: 7, + character: 0, + }, + end: { + line: 8, + character: 0, + }, + }, + selectionRange: { + start: { + line: 7, + character: 13, + }, + end: { + line: 7, + character: 30, + }, + }, + }, + ], + kind: 11, + name: 'TestInterface', + range: { + start: { + line: 5, + character: 0, + }, + end: { + line: 9, + character: 0, + }, + }, + selectionRange: { + start: { + line: 6, + character: 10, + }, + end: { + line: 6, + character: 23, + }, + }, + }, + { + children: [ + { + children: [], + kind: 7, + name: 'aNumber', + range: { + start: { + line: 11, + character: 0, + }, + end: { + line: 12, + character: 0, + }, + }, + selectionRange: { + start: { + line: 11, + character: 12, + }, + end: { + line: 11, + character: 19, + }, + }, + }, + { + children: [], + kind: 7, + name: 'aString', + range: { + start: { + line: 12, + character: 0, + }, + end: { + line: 13, + character: 0, + }, + }, + selectionRange: { + start: { + line: 12, + character: 11, + }, + end: { + line: 12, + character: 18, + }, + }, + }, + { + children: [], + kind: 7, + name: 'anAddress', + range: { + start: { + line: 13, + character: 0, + }, + end: { + line: 14, + character: 0, + }, + }, + selectionRange: { + start: { + line: 13, + character: 12, + }, + end: { + line: 13, + character: 21, + }, + }, + }, + ], + kind: 23, + name: 'TestStruct', + range: { + start: { + line: 9, + character: 0, + }, + end: { + line: 15, + character: 0, + }, + }, + selectionRange: { + start: { + line: 10, + character: 7, + }, + end: { + line: 10, + character: 17, + }, + }, + }, + { + children: [ + { + children: [], + kind: 7, + name: 'aNumber', + range: { + start: { + line: 17, + character: 0, + }, + end: { + line: 18, + character: 0, + }, + }, + selectionRange: { + start: { + line: 17, + character: 12, + }, + end: { + line: 17, + character: 19, + }, + }, + }, + { + children: [], + kind: 7, + name: 'aString', + range: { + start: { + line: 18, + character: 0, + }, + end: { + line: 19, + character: 0, + }, + }, + selectionRange: { + start: { + line: 18, + character: 11, + }, + end: { + line: 18, + character: 18, + }, + }, + }, + { + children: [], + kind: 7, + name: 'anAddress', + range: { + start: { + line: 19, + character: 0, + }, + end: { + line: 20, + character: 0, + }, + }, + selectionRange: { + start: { + line: 19, + character: 12, + }, + end: { + line: 19, + character: 21, + }, + }, + }, + ], + kind: 23, + name: 'TestStruct2', + range: { + start: { + line: 15, + character: 0, + }, + end: { + line: 21, + character: 0, + }, + }, + selectionRange: { + start: { + line: 16, + character: 7, + }, + end: { + line: 16, + character: 18, + }, + }, + }, + { + children: [], + kind: 14, + name: 'aConstant', + range: { + start: { + line: 21, + character: 0, + }, + end: { + line: 23, + character: 0, + }, + }, + selectionRange: { + start: { + line: 22, + character: 17, + }, + end: { + line: 22, + character: 26, + }, + }, + }, + { + children: [], + kind: 10, + name: 'Name', + range: { + start: { + line: 23, + character: 0, + }, + end: { + line: 28, + character: 0, + }, + }, + selectionRange: { + start: { + line: 24, + character: 5, + }, + end: { + line: 24, + character: 9, + }, + }, + }, + { + children: [], + kind: 24, + name: 'CustomError', + range: { + start: { + line: 28, + character: 0, + }, + end: { + line: 30, + character: 0, + }, + }, + selectionRange: { + start: { + line: 29, + character: 6, + }, + end: { + line: 29, + character: 17, + }, + }, + }, + { + children: [], + kind: 5, + name: 'TestLibrary', + range: { + start: { + line: 30, + character: 0, + }, + end: { + line: 32, + character: 0, + }, + }, + selectionRange: { + start: { + line: 31, + character: 8, + }, + end: { + line: 31, + character: 19, + }, + }, + }, + { + children: [ + { + children: [ + { + children: [], + kind: 13, + name: 'local', + range: { + start: { + line: 35, + character: 0, + }, + end: { + line: 35, + character: 21, + }, + }, + selectionRange: { + start: { + line: 35, + character: 16, + }, + end: { + line: 35, + character: 21, + }, + }, + }, + ], + kind: 9, + name: 'constructor', + range: { + start: { + line: 34, + character: 0, + }, + end: { + line: 38, + character: 0, + }, + }, + selectionRange: { + start: { + line: 34, + character: 4, + }, + end: { + line: 34, + character: 15, + }, + }, + }, + { + children: [], + kind: 12, + name: 'testModifier', + range: { + start: { + line: 38, + character: 0, + }, + end: { + line: 42, + character: 0, + }, + }, + selectionRange: { + start: { + line: 39, + character: 13, + }, + end: { + line: 39, + character: 25, + }, + }, + }, + { + children: [], + kind: 24, + name: 'TestEvent', + range: { + start: { + line: 42, + character: 0, + }, + end: { + line: 46, + character: 0, + }, + }, + selectionRange: { + start: { + line: 43, + character: 10, + }, + end: { + line: 43, + character: 19, + }, + }, + }, + { + children: [], + kind: 7, + name: 'contractAsMember', + range: { + start: { + line: 46, + character: 0, + }, + end: { + line: 48, + character: 0, + }, + }, + selectionRange: { + start: { + line: 47, + character: 17, + }, + end: { + line: 47, + character: 33, + }, + }, + }, + { + children: [], + kind: 7, + name: 'interfaceAsMember', + range: { + start: { + line: 48, + character: 0, + }, + end: { + line: 50, + character: 0, + }, + }, + selectionRange: { + start: { + line: 49, + character: 18, + }, + end: { + line: 49, + character: 35, + }, + }, + }, + { + children: [], + kind: 7, + name: 'structAsMember', + range: { + start: { + line: 50, + character: 0, + }, + end: { + line: 52, + character: 0, + }, + }, + selectionRange: { + start: { + line: 51, + character: 15, + }, + end: { + line: 51, + character: 29, + }, + }, + }, + { + children: [], + kind: 7, + name: 'aNumber', + range: { + start: { + line: 52, + character: 0, + }, + end: { + line: 54, + character: 0, + }, + }, + selectionRange: { + start: { + line: 53, + character: 12, + }, + end: { + line: 53, + character: 19, + }, + }, + }, + { + children: [], + kind: 7, + name: 'aString', + range: { + start: { + line: 54, + character: 0, + }, + end: { + line: 56, + character: 0, + }, + }, + selectionRange: { + start: { + line: 55, + character: 11, + }, + end: { + line: 55, + character: 18, + }, + }, + }, + { + children: [ + { + children: [], + kind: 13, + name: 'contractAsLocalVar', + range: { + start: { + line: 62, + character: 0, + }, + end: { + line: 62, + character: 39, + }, + }, + selectionRange: { + start: { + line: 62, + character: 21, + }, + end: { + line: 62, + character: 39, + }, + }, + }, + { + children: [], + kind: 13, + name: 'interfaceAsLocalVar', + range: { + start: { + line: 63, + character: 0, + }, + end: { + line: 64, + character: 41, + }, + }, + selectionRange: { + start: { + line: 64, + character: 22, + }, + end: { + line: 64, + character: 41, + }, + }, + }, + { + children: [], + kind: 13, + name: 'structAsLocalVar', + range: { + start: { + line: 65, + character: 0, + }, + end: { + line: 65, + character: 42, + }, + }, + selectionRange: { + start: { + line: 65, + character: 26, + }, + end: { + line: 65, + character: 42, + }, + }, + }, + { + children: [], + kind: 13, + name: 'afterUTF8', + range: { + start: { + line: 74, + character: 0, + }, + end: { + line: 77, + character: 35, + }, + }, + selectionRange: { + start: { + line: 77, + character: 26, + }, + end: { + line: 77, + character: 35, + }, + }, + }, + { + children: [], + kind: 12, + name: 'mult', + range: { + start: { + line: 82, + character: 0, + }, + end: { + line: 85, + character: 0, + }, + }, + selectionRange: { + start: { + line: 82, + character: 21, + }, + end: { + line: 82, + character: 25, + }, + }, + }, + ], + kind: 12, + name: 'testFunction', + range: { + start: { + line: 56, + character: 0, + }, + end: { + line: 87, + character: 0, + }, + }, + selectionRange: { + start: { + line: 57, + character: 13, + }, + end: { + line: 57, + character: 25, + }, + }, + }, + { + children: [], + kind: 12, + name: 'anotherFunction', + range: { + start: { + line: 87, + character: 0, + }, + end: { + line: 89, + character: 0, + }, + }, + selectionRange: { + start: { + line: 88, + character: 13, + }, + end: { + line: 88, + character: 28, + }, + }, + }, + { + children: [], + kind: 12, + name: 'fallback', + range: { + start: { + line: 89, + character: 0, + }, + end: { + line: 91, + character: 0, + }, + }, + selectionRange: { + start: { + line: 90, + character: 4, + }, + end: { + line: 90, + character: 12, + }, + }, + }, + { + children: [], + kind: 12, + name: 'receive', + range: { + start: { + line: 91, + character: 0, + }, + end: { + line: 93, + character: 0, + }, + }, + selectionRange: { + start: { + line: 92, + character: 4, + }, + end: { + line: 92, + character: 11, + }, + }, + }, + ], + kind: 5, + name: 'testContract', + range: { + start: { + line: 32, + character: 0, + }, + end: { + line: 94, + character: 0, + }, + }, + selectionRange: { + start: { + line: 33, + character: 9, + }, + end: { + line: 33, + character: 21, + }, + }, + }, + ]) + }) +}) + +describe('[projectless] documentSymbol', () => { + let testPath: string + + before(async () => { + client = await getInitializedClient() + + testPath = getProjectPath('projectless/src/documentSymbol/UnnamedFunction.sol') + + await client.openDocument(testPath) + }) + + after(async () => { + client.closeAllDocuments() + }) + + test('supports unnamed function definition', async function () { + const symbols = await client.getDocumentSymbols(toUri(testPath)) + + expect(symbols).to.deep.equal([ + { + children: [ + { + children: [], + kind: 12, + name: 'function', + range: { + start: { + line: 5, + character: 0, + }, + end: { + line: 6, + character: 0, + }, + }, + selectionRange: { + start: { + line: 5, + character: 4, + }, + end: { + line: 5, + character: 12, + }, + }, + }, + ], + kind: 5, + name: 'UnnamedTest', + range: { + start: { + line: 3, + character: 0, + }, + end: { + line: 7, + character: 0, + }, + }, + selectionRange: { + start: { + line: 4, + character: 9, + }, + end: { + line: 4, + character: 20, + }, + }, + }, + ]) + }) +}) diff --git a/test/protocol/test/textDocument/semanticTokens/full.test.ts b/test/protocol/test/textDocument/semanticTokens/full.test.ts new file mode 100644 index 00000000..812c1211 --- /dev/null +++ b/test/protocol/test/textDocument/semanticTokens/full.test.ts @@ -0,0 +1,37 @@ +import { expect } from 'chai' +import { test } from 'mocha' +import { TestLanguageClient } from '../../../src/TestLanguageClient' +import { getInitializedClient } from '../../client' +import { getProjectPath } from '../../helpers' +import { toUri } from '../../../src/helpers' + +let client!: TestLanguageClient + +describe('[hardhat] semanticTokens/full', () => { + let testPath: string + + before(async () => { + client = await getInitializedClient() + + testPath = getProjectPath('hardhat/contracts/semanticTokens/full/SemanticTokens.sol') + + await client.openDocument(testPath) + }) + + after(async () => { + client.closeAllDocuments() + }) + + test('provides highlighting for types, keywords, functions, numbers and strings', async function () { + const semanticTokens = await client.getSemanticTokensFull(toUri(testPath)) + + expect(semanticTokens).to.deep.equal({ + data: [ + 4, 5, 10, 2, 0, 2, 10, 13, 2, 0, 2, 7, 10, 2, 0, 6, 5, 8, 2, 0, 6, 6, 11, 2, 0, 2, 8, 9, 2, 0, 2, 9, 12, 2, 0, + 1, 10, 9, 2, 0, 1, 8, 12, 2, 0, 0, 35, 13, 2, 0, 0, 37, 10, 2, 0, 3, 4, 12, 2, 0, 2, 4, 13, 2, 0, 2, 4, 10, 2, + 0, 6, 13, 12, 4, 0, 1, 8, 12, 2, 0, 1, 8, 13, 2, 0, 1, 8, 10, 2, 0, 2, 8, 12, 2, 0, 2, 8, 13, 2, 0, 1, 8, 10, 2, + 0, 10, 13, 9, 2, 0, 4, 8, 10, 2, 0, 2, 8, 15, 4, 0, 3, 13, 15, 4, 0, + ], + }) + }) +})