├── .markdownlint.json ├── images ├── icon.png ├── mdn.png ├── cfdocs.png ├── lucee.png ├── openbd.png ├── coldfusion.png ├── cfdocs_leaderboard.png ├── cfdocs_definition-peek.png ├── cfdocs_leaderboard_hover.png ├── cfdocs_workspace-symbols.png ├── cfdocs_leaderboard_completion.png ├── cfdocs_leaderboard_signature.png └── cfdocs_leaderboard_document-symbols.png ├── src ├── entities │ ├── css │ │ ├── property.ts │ │ ├── languageFacts.ts │ │ └── cssLanguageTypes.ts │ ├── operator.ts │ ├── html │ │ ├── htmlTag.ts │ │ ├── languageFacts.ts │ │ └── htmlLanguageTypes.ts │ ├── parameter.ts │ ├── signature.ts │ ├── keyword.ts │ ├── attribute.ts │ ├── docblock.ts │ ├── query.ts │ ├── function.ts │ ├── cgi.ts │ ├── globals.ts │ ├── catch.ts │ ├── dataType.ts │ ├── property.ts │ └── scope.ts ├── features │ ├── docBlocker │ │ ├── block │ │ │ ├── property.ts │ │ │ ├── component.ts │ │ │ └── function.ts │ │ ├── documenter.ts │ │ ├── block.ts │ │ ├── doc.ts │ │ └── docCompletionProvider.ts │ ├── workspaceSymbolProvider.ts │ ├── comment.ts │ ├── commands.ts │ ├── documentLinkProvider.ts │ ├── documentSymbolProvider.ts │ ├── signatureHelpProvider.ts │ ├── typeDefinitionProvider.ts │ └── colorProvider.ts └── utils │ ├── dateUtil.ts │ ├── collections.ts │ ├── cfdocs │ ├── multiSignatures.ts │ ├── cfmlEngine.ts │ └── definitionInfo.ts │ ├── documentUtil.ts │ ├── textUtil.ts │ └── fileUtil.ts ├── .gitignore ├── .vscodeignore ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── tsconfig.json ├── language-configuration.json ├── LICENSE ├── tslint.json ├── CONTRIBUTING.md ├── snippets └── snippets.json ├── .github └── workflows │ └── release.yml ├── CHANGELOG.md └── resources └── schemas └── cfdocs.schema.json /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, 3 | "MD031": false 4 | } -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/icon.png -------------------------------------------------------------------------------- /images/mdn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/mdn.png -------------------------------------------------------------------------------- /images/cfdocs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/cfdocs.png -------------------------------------------------------------------------------- /images/lucee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/lucee.png -------------------------------------------------------------------------------- /images/openbd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/openbd.png -------------------------------------------------------------------------------- /images/coldfusion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/coldfusion.png -------------------------------------------------------------------------------- /src/entities/css/property.ts: -------------------------------------------------------------------------------- 1 | export const cssPropertyPattern = /\b(([a-z-]+)\s*:\s*)([^;{}]+?)\s*(?=[;}])/gi; 2 | -------------------------------------------------------------------------------- /images/cfdocs_leaderboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/cfdocs_leaderboard.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .history/ 2 | .vscode/cSpell.json 3 | .DS_Store 4 | Thumbs.db 5 | node_modules/ 6 | out/ 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /images/cfdocs_definition-peek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/cfdocs_definition-peek.png -------------------------------------------------------------------------------- /images/cfdocs_leaderboard_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/cfdocs_leaderboard_hover.png -------------------------------------------------------------------------------- /images/cfdocs_workspace-symbols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/cfdocs_workspace-symbols.png -------------------------------------------------------------------------------- /images/cfdocs_leaderboard_completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/cfdocs_leaderboard_completion.png -------------------------------------------------------------------------------- /images/cfdocs_leaderboard_signature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/cfdocs_leaderboard_signature.png -------------------------------------------------------------------------------- /images/cfdocs_leaderboard_document-symbols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KamasamaK/vscode-cfml/HEAD/images/cfdocs_leaderboard_document-symbols.png -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .sonarlint/** 2 | .github/** 3 | .history/** 4 | .vscode/** 5 | .vscode-test/** 6 | test/** 7 | src/** 8 | **/*.map 9 | .gitignore 10 | .markdownlint.json 11 | tsconfig.json 12 | tslint.json 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "ms-vscode.vscode-typescript-tslint-plugin", 6 | "DavidAnson.vscode-markdownlint", 7 | "eg2.vscode-npm-script" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/features/docBlocker/block/property.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "../block"; 2 | import { Doc, DocType } from "../doc"; 3 | 4 | 5 | export default class Property extends Block { 6 | 7 | protected pattern: RegExp = /^(\s*property)\s+/i; 8 | 9 | public constructDoc(): Doc { 10 | return new Doc(DocType.Property, this.document.uri); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/features/docBlocker/block/component.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "../block"; 2 | import { Doc, DocType } from "../doc"; 3 | 4 | export default class Component extends Block { 5 | protected pattern: RegExp = /^(\s*(?:component|interface))\b[^{]*\{/i; 6 | 7 | public constructDoc(): Doc { 8 | return new Doc(this.component.isInterface ? DocType.Interface : DocType.Component, this.document.uri); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2019", 5 | "outDir": "./out", 6 | "lib": [ 7 | "ES2019" 8 | ], 9 | "sourceMap": true, 10 | "alwaysStrict": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": true, 15 | "esModuleInterop": true 16 | }, 17 | "exclude": [ 18 | "node_modules" 19 | ], 20 | "plugins": [ 21 | { 22 | "name": "typescript-tslint-plugin" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "files.trimTrailingWhitespace": true, 10 | "[markdown]": { 11 | "files.trimTrailingWhitespace": false 12 | }, 13 | "editor.detectIndentation": false, 14 | "editor.tabSize": 2, 15 | "editor.insertSpaces": true, 16 | "typescript.tsc.autoDetect": "off", 17 | "typescript.tsdk": "node_modules\\typescript\\lib", 18 | "cSpell.words": [ 19 | "struct" 20 | ] 21 | } -------------------------------------------------------------------------------- /src/entities/operator.ts: -------------------------------------------------------------------------------- 1 | import { MySet } from "../utils/collections"; 2 | 3 | export const operators = new MySet([ 4 | "+", 5 | "-", 6 | "*", 7 | "/", 8 | "^", 9 | "%", 10 | "MOD", 11 | "\\", 12 | "++", 13 | "--", 14 | "+=", 15 | "-=", 16 | "*=", 17 | "/=", 18 | "!", 19 | "NOT", 20 | "AND", 21 | "&&", 22 | "||", 23 | "XOR", 24 | "EQ", 25 | "==", 26 | "===", 27 | "NEQ", 28 | "<>", 29 | "!=", 30 | "!==", 31 | "GT", 32 | ">", 33 | "LT", 34 | "<", 35 | "GTE", 36 | ">=", 37 | "LTE", 38 | "<=", 39 | "CONTAINS", 40 | "CT", // Lucee-only 41 | "DOES NOT CONTAIN", 42 | "NCT", // Lucee-only 43 | "&", 44 | "&=", 45 | ".", 46 | "?:", 47 | ".?", 48 | "..." 49 | ]); 50 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": [ "/*", "*/" ] 5 | }, 6 | "brackets": [ 7 | ["{", "}"], 8 | ["[", "]"], 9 | ["(", ")"] 10 | ], 11 | "autoClosingPairs": [ 12 | ["{", "}"], 13 | ["[", "]"], 14 | ["(", ")"], 15 | ["\"", "\""], 16 | ["'", "'"], 17 | ["#", "#"] 18 | ], 19 | "surroundingPairs": [ 20 | ["{", "}"], 21 | ["[", "]"], 22 | ["(", ")"], 23 | ["\"", "\""], 24 | ["'", "'"], 25 | ["#", "#"] 26 | ], 27 | "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\-`~!@#%\\^&*()=+[{\\]}\\\\|;:'\",.<>/?\\s]+)", 28 | "folding": { 29 | "markers": { 30 | "start": "^\\s*(?:|//\\s*#?region\\b)", 31 | "end": "^\\s*(?:|//\\s*#?endregion\\b)" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/entities/html/htmlTag.ts: -------------------------------------------------------------------------------- 1 | import { AttributeQuoteType } from "../attribute"; 2 | import { getQuote } from "../../utils/textUtil"; 3 | import { IAttributeData as HTMLAttributeData } from "./htmlLanguageTypes"; 4 | import { getAttribute } from "./languageFacts"; 5 | 6 | export const HTML_EMPTY_ELEMENTS: string[] = ["area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]; 7 | 8 | export function constructHTMLAttributeSnippet(tagName: string, attributeName: string, attributeQuoteType: AttributeQuoteType = AttributeQuoteType.Double): string { 9 | const attribute: HTMLAttributeData = getAttribute(tagName, attributeName); 10 | 11 | if (!attribute) { 12 | return ""; 13 | } 14 | 15 | if (attribute.valueSet === "v") { 16 | return attributeName; 17 | } 18 | 19 | const quoteStr: string = getQuote(attributeQuoteType); 20 | 21 | return `${attributeName}=${quoteStr}\${1}${quoteStr}`; 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "panel": "dedicated" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "build", 23 | "group": "build", 24 | "problemMatcher": "$tsc", 25 | "presentation": { 26 | "reveal": "never", 27 | "panel": "dedicated" 28 | } 29 | }, 30 | { 31 | "type": "npm", 32 | "script": "lint", 33 | "problemMatcher": { 34 | "base": "$tslint5", 35 | "fileLocation": "relative" 36 | } 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Launch Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "stopOnEntry": false, 17 | "sourceMaps": true, 18 | "outFiles": [ 19 | "${workspaceFolder}/out/**/*.js" 20 | ], 21 | "skipFiles": ["/**"], 22 | "preLaunchTask": "npm: watch" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 KamasamaK 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "adjacent-overload-signatures": true, 4 | "array-type": [ 5 | true, 6 | "array" 7 | ], 8 | "arrow-parens": true, 9 | "class-name": true, 10 | "comment-format": [ 11 | true, 12 | "check-space" 13 | ], 14 | "completed-docs": [ 15 | true, 16 | "functions", 17 | "methods" 18 | ], 19 | "curly": true, 20 | "indent": [ 21 | true, 22 | "spaces", 23 | 4 24 | ], 25 | "jsdoc-format": [true, "check-multiline-start"], 26 | "new-parens": true, 27 | "no-any": true, 28 | "no-unused-expression": true, 29 | "no-duplicate-variable": true, 30 | "no-trailing-whitespace": true, 31 | "no-var-keyword": true, 32 | "only-arrow-functions": [ 33 | true, 34 | "allow-declarations", 35 | "allow-named-functions" 36 | ], 37 | "quotemark": [ 38 | true, 39 | "double", 40 | "avoid-escape", 41 | "avoid-template" 42 | ], 43 | "semicolon": [ 44 | true, 45 | "always" 46 | ], 47 | "space-before-function-paren": [ 48 | true, 49 | { 50 | "anonymous": "always", 51 | "named": "never" 52 | } 53 | ], 54 | "triple-equals": true, 55 | "prefer-for-of": true 56 | } 57 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you would like to contribute enhancements or fixes, please read this document first. 4 | 5 | ## Setup 6 | 7 | - Fork [KamasamaK/vscode-cfml](https://github.com/KamasamaK/vscode-cfml) 8 | - Clone your forked repository 9 | - Install [Node.js with npm](https://nodejs.org) if not already installed 10 | - Open this project as the workspace in VS Code 11 | - Install the recommended extensions in `.vscode/extensions.json` 12 | - Run `npm install` at workspace root to install dependencies 13 | 14 | ## Working 15 | 16 | - It is recommended to work on a separate feature branch created from the latest `master`. 17 | - To debug, run the `Launch Extension` debug target in the [Debug View](https://code.visualstudio.com/docs/editor/debugging). This will: 18 | - Launch the `preLaunchTask` task to compile the extension 19 | - Launch a new VS Code instance with the `vscode-cfml` extension loaded 20 | - You will see a notification saying the development version of `vscode-cfml` overwrites the bundled version of `vscode-cfml` if you have an older version installed 21 | - Make a pull request to the upstream `master` 22 | 23 | ## Guidelines 24 | 25 | - Code should pass **TSLint** and **markdownlint** with the included configuration. 26 | - Please use descriptive variable names and only use well-known abbreviations. 27 | -------------------------------------------------------------------------------- /src/entities/parameter.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from "./dataType"; 2 | import { Argument } from "./userFunction"; 3 | import { COMPONENT_EXT } from "./component"; 4 | import * as path from "path"; 5 | 6 | export interface Parameter { 7 | name: string; 8 | description: string; 9 | dataType: DataType; 10 | required: boolean; 11 | default?: string; 12 | enumeratedValues?: string[]; 13 | } 14 | 15 | export const namedParameterPattern: RegExp = /^\s*([\w$]+)\s*=(?!=)/; 16 | 17 | /** 18 | * Gets the parameter's name 19 | * @param param The Parameter object from which to get the name 20 | */ 21 | export function getParameterName(param: Parameter): string { 22 | return param.name.split("=")[0]; 23 | } 24 | 25 | /** 26 | * Constructs a string label representation of a parameter 27 | * @param param The Parameter object on which to base the label 28 | */ 29 | export function constructParameterLabel(param: Parameter): string { 30 | let paramLabel = getParameterName(param); 31 | if (!param.required) { 32 | paramLabel += "?"; 33 | } 34 | 35 | let paramType: string = param.dataType.toLowerCase(); 36 | if (param.dataType === DataType.Component) { 37 | const arg: Argument = param as Argument; 38 | if (arg.dataTypeComponentUri) { 39 | paramType = path.basename(arg.dataTypeComponentUri.fsPath, COMPONENT_EXT); 40 | } 41 | } 42 | 43 | paramLabel += ": " + paramType; 44 | 45 | return paramLabel; 46 | } 47 | -------------------------------------------------------------------------------- /src/entities/signature.ts: -------------------------------------------------------------------------------- 1 | import { Parameter, constructParameterLabel } from "./parameter"; 2 | import { Function } from "./function"; 3 | 4 | export interface Signature { 5 | parameters: Parameter[]; 6 | description?: string; 7 | } 8 | 9 | /** 10 | * Constructs the beginning part of the signature label 11 | * @param func The function from which to construct the parameter prefix 12 | */ 13 | export function constructSignatureLabelParamsPrefix(func: Function): string { 14 | // TODO: If UserFunction, use ComponentName.functionName based on location 15 | return func.name; 16 | } 17 | 18 | /** 19 | * Constructs a string label representation of the parameters in a signature 20 | * @param parameters The parameters on which to base the label 21 | */ 22 | export function constructSignatureLabelParamsPart(parameters: Parameter[]): string { 23 | return parameters.map(constructParameterLabel).join(", "); 24 | } 25 | 26 | /** 27 | * Gets offset tuple ranges for the signature param label 28 | * @param parameters The parameters in a signature 29 | */ 30 | export function getSignatureParamsLabelOffsetTuples(parameters: Parameter[]): [number, number][] { 31 | let endIdx: number = -2; 32 | 33 | return parameters.map(constructParameterLabel).map((paramLabel: string) => { 34 | const startIdx: number = endIdx + 2; 35 | endIdx = startIdx + paramLabel.length; 36 | 37 | return [startIdx, endIdx] as [number, number]; 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/entities/keyword.ts: -------------------------------------------------------------------------------- 1 | export interface KeywordDetails { 2 | description: string; 3 | onlyScript: boolean; 4 | links: string[]; 5 | } 6 | 7 | export interface Keywords { 8 | [keyword: string]: KeywordDetails; 9 | } 10 | 11 | export const keywords: Keywords = { 12 | "var": { 13 | description: "", 14 | onlyScript: false, 15 | links: [] 16 | }, 17 | "for": { 18 | description: "", 19 | onlyScript: true, 20 | links: [] 21 | }, 22 | "default": { 23 | description: "", 24 | onlyScript: true, 25 | links: [] 26 | }, 27 | "continue": { 28 | description: "", 29 | onlyScript: true, 30 | links: [] 31 | }, 32 | "import": { 33 | description: "", 34 | onlyScript: true, 35 | links: [] 36 | }, 37 | "finally": { 38 | description: "", 39 | onlyScript: true, 40 | links: [] 41 | }, 42 | "interface": { 43 | description: "", 44 | onlyScript: true, 45 | links: [] 46 | }, 47 | "pageencoding": { 48 | description: "", 49 | onlyScript: true, 50 | links: [] 51 | }, 52 | "abort": { 53 | description: "", 54 | onlyScript: true, 55 | links: [] 56 | }, 57 | "exist": { 58 | description: "", 59 | onlyScript: true, 60 | links: [] 61 | }, 62 | "true": { 63 | description: "", 64 | onlyScript: false, 65 | links: [] 66 | }, 67 | "false": { 68 | description: "", 69 | onlyScript: false, 70 | links: [] 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/features/docBlocker/block/function.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "../block"; 2 | import { Doc, DocType } from "../doc"; 3 | import { Position } from "vscode"; 4 | import { UserFunction, UserFunctionSignature, Argument } from "../../../entities/userFunction"; 5 | 6 | /** 7 | * Represents a function code block 8 | * 9 | * This is probably going to be the most complicated of all the 10 | * blocks as function signatures tend to be the most complex and 11 | * varied 12 | */ 13 | export default class FunctionBlock extends Block { 14 | 15 | protected pattern: RegExp = /^(\s*)(?:\b(?:private|package|public|remote|static|final|abstract|default)\s+)?(?:\b(?:private|package|public|remote|static|final|abstract|default)\s+)?(?:\b(?:[A-Za-z0-9_\.$]+)\s+)?function\s+(?:[_$a-zA-Z][$\w]*)\s*(?:\((?:=\s*\{|[^{])*)[\{;]/i; 16 | 17 | public constructDoc(): Doc { 18 | let doc = new Doc(DocType.Function, this.document.uri); 19 | 20 | const positionOffset: number = this.document.offsetAt(this.position); 21 | const patternMatch: RegExpExecArray = this.pattern.exec(this.suffix); 22 | if (patternMatch) { 23 | const declaration: Position = this.document.positionAt(positionOffset + patternMatch[1].length + 1); 24 | this.component.functions.filter((func: UserFunction) => { 25 | return func.location.range.contains(declaration); 26 | }).forEach((func: UserFunction) => { 27 | func.signatures.forEach((sig: UserFunctionSignature) => { 28 | sig.parameters.forEach((arg: Argument) => { 29 | doc.params.push(arg.name); 30 | }); 31 | }); 32 | }); 33 | } 34 | 35 | return doc; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/features/docBlocker/documenter.ts: -------------------------------------------------------------------------------- 1 | import { Position, SnippetString, TextDocument } from "vscode"; 2 | import FunctionBlock from "./block/function"; 3 | import Property from "./block/property"; 4 | import Component from "./block/component"; 5 | import { Doc, DocType } from "./doc"; 6 | 7 | /** 8 | * Check which type of DocBlock we need and instruct the components to build the 9 | * snippet and pass it back 10 | */ 11 | export default class Documenter { 12 | /** 13 | * The target position of the comment block 14 | */ 15 | protected targetPosition: Position; 16 | 17 | /** 18 | * The document to pass to each editor 19 | */ 20 | protected document: TextDocument; 21 | 22 | /** 23 | * Creates an instance of Documenter. 24 | * 25 | * @param position 26 | * @param editor 27 | */ 28 | public constructor(position: Position, document: TextDocument) { 29 | this.targetPosition = position; 30 | this.document = document; 31 | } 32 | 33 | /** 34 | * Load and test each type of signature to see if they can trigger and 35 | * if not load an empty block 36 | */ 37 | public autoDocument(): SnippetString { 38 | let func = new FunctionBlock(this.targetPosition, this.document); 39 | if (func.test()) { 40 | return func.constructDoc().build(); 41 | } 42 | 43 | let prop = new Property(this.targetPosition, this.document); 44 | if (prop.test()) { 45 | return prop.constructDoc().build(); 46 | } 47 | 48 | let comp = new Component(this.targetPosition, this.document); 49 | if (comp.test()) { 50 | return comp.constructDoc().build(); 51 | } 52 | 53 | return new Doc(DocType.Unknown, this.document.uri).build(true); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/dateUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets the current date as a formatted string 3 | * 4 | * @return Formatted date 5 | */ 6 | function getCurrentDateFormatted(): string { 7 | const currDate = new Date(); 8 | 9 | const year: string = currDate.getFullYear().toString(); 10 | let month: string = (currDate.getMonth() + 1).toString(); 11 | month = (month.length === 1) ? "0" + month : month; 12 | let day: string = currDate.getDate().toString(); 13 | day = (day.length === 1) ? "0" + day : day; 14 | 15 | return year + "-" + month + "-" + day; 16 | } 17 | 18 | /** 19 | * Gets the current time as a formatted string 20 | * 21 | * @return Formatted time 22 | */ 23 | function getCurrentTimeFormatted(includeMilliseconds: boolean = false): string { 24 | const currDate = new Date(); 25 | 26 | let hours: string = currDate.getHours().toString(); 27 | hours = (hours.length === 1) ? "0" + hours : hours; 28 | let minutes: string = currDate.getMinutes().toString(); 29 | minutes = (minutes.length === 1) ? "0" + minutes : minutes; 30 | let seconds: string = currDate.getSeconds().toString(); 31 | seconds = (seconds.length === 1) ? "0" + seconds : seconds; 32 | 33 | let formattedTime: string = hours + ":" + minutes + ":" + seconds; 34 | if (includeMilliseconds) { 35 | let milliseconds: string = currDate.getMilliseconds().toString(); 36 | while (milliseconds.length < 3) { 37 | milliseconds = "0" + milliseconds; 38 | } 39 | formattedTime += "." + milliseconds; 40 | } 41 | 42 | return formattedTime; 43 | } 44 | 45 | /** 46 | * Gets the current datetime as a formatted string 47 | * 48 | * @return Formatted datetime 49 | */ 50 | export function getCurrentDateTimeFormatted(): string { 51 | return getCurrentDateFormatted() + " " + getCurrentTimeFormatted(); 52 | } 53 | -------------------------------------------------------------------------------- /src/entities/css/languageFacts.ts: -------------------------------------------------------------------------------- 1 | /** Adopted from https://github.com/Microsoft/vscode-css-languageservice/blob/27f369f0d527b1952689e223960f779e89457374/src/languageFacts/index.ts */ 2 | 3 | import * as cssLanguageTypes from "./cssLanguageTypes"; 4 | import * as cssLanguageFacts from "vscode-css-languageservice/lib/umd/languageFacts/index"; 5 | 6 | export const cssWordRegex: RegExp = /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/; 7 | 8 | export const cssDataManager: cssLanguageTypes.ICSSDataManager = cssLanguageFacts.cssDataManager; 9 | 10 | export const cssColors: { [name: string]: string } = cssLanguageFacts.colors; 11 | 12 | function getEntryStatus(status: cssLanguageTypes.EntryStatus): string { 13 | switch (status) { 14 | case "experimental": 15 | return "⚠️ Property is experimental. Be cautious when using it.\n\n"; 16 | case "nonstandard": 17 | return "🚨️ Property is nonstandard. Avoid using it.\n\n"; 18 | case "obsolete": 19 | return "🚨️️️ Property is obsolete. Avoid using it.\n\n"; 20 | default: 21 | return ""; 22 | } 23 | } 24 | 25 | /** 26 | * Constructs a description for the given CSS entry 27 | * @param entry A CSS entry object 28 | */ 29 | export function getEntryDescription(entry: cssLanguageTypes.IEntry): string | null { 30 | if (!entry.description || entry.description === "") { 31 | return null; 32 | } 33 | 34 | let result: string = ""; 35 | 36 | if (entry.status) { 37 | result += getEntryStatus(entry.status); 38 | } 39 | 40 | result += entry.description; 41 | 42 | const browserLabel = cssLanguageFacts.getBrowserLabel(entry.browsers); 43 | if (browserLabel) { 44 | result += `\n(${browserLabel})`; 45 | } 46 | 47 | /* 48 | if ("syntax" in entry) { 49 | result += `\n\nSyntax: ${entry.syntax}`; 50 | } 51 | */ 52 | 53 | return result; 54 | } 55 | -------------------------------------------------------------------------------- /src/entities/html/languageFacts.ts: -------------------------------------------------------------------------------- 1 | import { IHTMLDataProvider, ITagData, IAttributeData } from "./htmlLanguageTypes"; 2 | import * as htmlData from "vscode-html-languageservice/lib/umd/languageFacts/data/html5"; 3 | import { equalsIgnoreCase } from "../../utils/textUtil"; 4 | 5 | export const htmlDataProvider: IHTMLDataProvider = htmlData.getHTML5DataProvider(); 6 | 7 | // Recreate maps since they are private 8 | const htmlTagMap: { [t: string]: ITagData } = {}; 9 | 10 | htmlData.HTML5_TAGS.forEach((t) => { 11 | htmlTagMap[t.name] = t; 12 | }); 13 | 14 | /** 15 | * Whether the given name is a known HTML tag 16 | * @param name Tag name to check 17 | */ 18 | export function isKnownTag(name: string): boolean { 19 | return name.toLowerCase() in htmlTagMap; 20 | } 21 | 22 | // isStandardTag (when status becomes available) 23 | 24 | /** 25 | * Gets HTML tag data 26 | * @param name The tag name 27 | */ 28 | export function getTag(name: string): ITagData | undefined { 29 | return htmlTagMap[name.toLowerCase()]; 30 | } 31 | 32 | /** 33 | * Whether the tag with the given name has an attribute with the given name 34 | * @param tagName The tag name 35 | * @param attributeName The attribute name 36 | */ 37 | export function hasAttribute(tagName: string, attributeName: string): boolean { 38 | return htmlDataProvider.provideAttributes(tagName.toLowerCase()).some((attr: IAttributeData) => { 39 | return equalsIgnoreCase(attr.name, attributeName); 40 | }); 41 | } 42 | 43 | /** 44 | * Gets HTML tag attribute data 45 | * @param tagName The tag name 46 | * @param attributeName The attribute name 47 | */ 48 | export function getAttribute(tagName: string, attributeName: string): IAttributeData | undefined { 49 | return htmlDataProvider.provideAttributes(tagName.toLowerCase()).find((attr: IAttributeData) => { 50 | return equalsIgnoreCase(attr.name, attributeName); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/collections.ts: -------------------------------------------------------------------------------- 1 | import { equalsIgnoreCase } from "./textUtil"; 2 | 3 | export class MyMap extends Map { 4 | /** 5 | * Creates a new `MyMap` with all elements that pass the test implemented by the provided function. 6 | * @param callbackfn A predicate to test each key-value pair of the map 7 | */ 8 | public filter(callbackfn: (value: V, key: K, map: MyMap) => boolean): MyMap { 9 | let myMap = new MyMap(); 10 | this.forEach((value: V, key: K, map: MyMap) => { 11 | if (callbackfn(value, key, map)) { 12 | myMap.set(key, value); 13 | } 14 | }); 15 | 16 | return myMap; 17 | } 18 | } 19 | 20 | export class MySet extends Set { 21 | /** 22 | * Creates a new `MySet` with all elements that pass the test implemented by the provided function. 23 | * @param callbackfn A predicate to test each element of the set 24 | */ 25 | public filter(callbackfn: (value: T, value2: T, set: MySet) => boolean): MySet { 26 | let mySet = new MySet(); 27 | this.forEach((value: T, value2: T, set: MySet) => { 28 | if (callbackfn(value, value2, set)) { 29 | mySet.add(value); 30 | } 31 | }); 32 | 33 | return mySet; 34 | } 35 | } 36 | 37 | /** 38 | * Returns whether the given `str` is contained within the given `arr` ignoring case 39 | * @param arr The string array within which to search 40 | * @param str The string for which to check 41 | */ 42 | export function stringArrayIncludesIgnoreCase(arr: string[], str: string): boolean { 43 | return arr.some((val: string) => { 44 | return equalsIgnoreCase(val, str); 45 | }); 46 | } 47 | 48 | // TODO: Find a better place for this 49 | export interface NameWithOptionalValue { 50 | name: string; 51 | value?: T; 52 | } 53 | 54 | // TODO: Find a better place for this 55 | export enum SearchMode { 56 | StartsWith, 57 | Contains, 58 | EqualTo, 59 | } 60 | -------------------------------------------------------------------------------- /src/features/docBlocker/block.ts: -------------------------------------------------------------------------------- 1 | import { Range, Position, TextDocument } from "vscode"; 2 | import { Doc } from "./doc"; 3 | import { Component } from "../../entities/component"; 4 | import { getComponent } from "../cachedEntities"; 5 | 6 | /** 7 | * Represents a potential code block. 8 | * 9 | * This abstract class serves as a base class that includes 10 | * helpers for dealing with blocks of code and has the basic interface 11 | * for working with the documenter object 12 | */ 13 | export abstract class Block { 14 | /** 15 | * Regex pattern for the block declaration match 16 | */ 17 | protected pattern: RegExp; 18 | 19 | /** 20 | * The position of the starting signature 21 | */ 22 | protected position: Position; 23 | 24 | /** 25 | * Text document which we'll need to do things like 26 | * get text and ranges and things between ranges 27 | */ 28 | protected document: TextDocument; 29 | 30 | /** 31 | * The whole signature string ready for parsing 32 | */ 33 | protected suffix: string; 34 | 35 | /** 36 | * The component object 37 | */ 38 | protected component: Component; 39 | 40 | /** 41 | * Creates an instance of Block. 42 | * 43 | * @param position The current position from which the DocBlock will be inserted 44 | * @param document The document object in which the DocBlock is being created 45 | */ 46 | public constructor(position: Position, document: TextDocument) { 47 | this.position = position; 48 | this.document = document; 49 | this.setSuffix(document.getText(new Range(position, document.positionAt(document.getText().length)))); 50 | this.component = getComponent(document.uri); 51 | } 52 | 53 | /** 54 | * Set the suffix text. 55 | * @param suffix The document text that occurs after this.position 56 | */ 57 | public setSuffix(suffix: string): Block { 58 | this.suffix = suffix; 59 | return this; 60 | } 61 | 62 | /** 63 | * This should be a simple test to determine whether this matches 64 | * our intended block declaration and we can proceed to properly 65 | * document 66 | */ 67 | public test(): boolean { 68 | return this.pattern.test(this.suffix); 69 | } 70 | 71 | /** 72 | * This is where we parse the code block into a Doc 73 | * object which represents our snippet 74 | */ 75 | public abstract constructDoc(): Doc; 76 | } 77 | -------------------------------------------------------------------------------- /src/features/workspaceSymbolProvider.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { CancellationToken, Location, Position, SymbolInformation, SymbolKind, TextDocument, TextEditor, Uri, window, workspace, WorkspaceSymbolProvider } from "vscode"; 3 | import { LANGUAGE_ID } from "../cfmlMain"; 4 | import { Component, COMPONENT_EXT } from "../entities/component"; 5 | import { UserFunction } from "../entities/userFunction"; 6 | import { equalsIgnoreCase } from "../utils/textUtil"; 7 | import * as cachedEntity from "./cachedEntities"; 8 | 9 | export default class CFMLWorkspaceSymbolProvider implements WorkspaceSymbolProvider { 10 | 11 | /** 12 | * Workspace-wide search for a symbol matching the given query string. 13 | * @param query A non-empty query string. 14 | * @param _token A cancellation token. 15 | */ 16 | public async provideWorkspaceSymbols(query: string, _token: CancellationToken): Promise { 17 | let workspaceSymbols: SymbolInformation[] = []; 18 | if (query === "") { 19 | return workspaceSymbols; 20 | } 21 | 22 | let uri: Uri | undefined = undefined; 23 | const editor: TextEditor = window.activeTextEditor; 24 | if (editor) { 25 | const document: TextDocument = editor.document; 26 | if (document && document.languageId === LANGUAGE_ID) { 27 | uri = document.uri; 28 | } 29 | } 30 | if (!uri) { 31 | const documents: ReadonlyArray = workspace.textDocuments; 32 | for (const document of documents) { 33 | if (document.languageId === LANGUAGE_ID) { 34 | uri = document.uri; 35 | break; 36 | } 37 | } 38 | } 39 | 40 | if (!uri) { 41 | return workspaceSymbols; 42 | } 43 | 44 | const userFunctions: UserFunction[] = cachedEntity.searchAllFunctionNames(query); 45 | 46 | workspaceSymbols = workspaceSymbols.concat( 47 | userFunctions.map((userFunction: UserFunction) => { 48 | return new SymbolInformation( 49 | userFunction.name + "()", 50 | equalsIgnoreCase(userFunction.name, "init") ? SymbolKind.Constructor : SymbolKind.Function, 51 | path.basename(userFunction.location.uri.fsPath, COMPONENT_EXT), 52 | userFunction.location 53 | ); 54 | }) 55 | ); 56 | 57 | const components: Component[] = cachedEntity.searchAllComponentNames(query); 58 | workspaceSymbols = workspaceSymbols.concat( 59 | components.map((component: Component) => { 60 | return new SymbolInformation( 61 | path.basename(component.uri.fsPath, COMPONENT_EXT), 62 | component.isInterface ? SymbolKind.Interface : SymbolKind.Class, 63 | "", 64 | new Location(component.uri, new Position(0, 0)) 65 | ); 66 | }) 67 | ); 68 | 69 | return workspaceSymbols; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/features/comment.ts: -------------------------------------------------------------------------------- 1 | import { Position, languages, commands, window, TextEditor, LanguageConfiguration, TextDocument, CharacterPair } from "vscode"; 2 | import { LANGUAGE_ID } from "../cfmlMain"; 3 | import { isInCfScript, isCfcFile } from "../utils/contextUtil"; 4 | import { getComponent, hasComponent } from "./cachedEntities"; 5 | 6 | export enum CommentType { 7 | Line, 8 | Block 9 | } 10 | 11 | export interface CFMLCommentRules { 12 | scriptBlockComment: CharacterPair; 13 | scriptLineComment: string; 14 | tagBlockComment: CharacterPair; 15 | } 16 | 17 | export interface CommentContext { 18 | inComment: boolean; 19 | activeComment: string | CharacterPair; 20 | commentType: CommentType; 21 | start: Position; 22 | } 23 | 24 | export const cfmlCommentRules: CFMLCommentRules = { 25 | scriptBlockComment: ["/*", "*/"], 26 | scriptLineComment: "//", 27 | tagBlockComment: [""] 28 | }; 29 | 30 | /** 31 | * Returns whether to use CFML tag comment 32 | * @param document The TextDocument in which the selection is made 33 | * @param startPosition The position at which the comment starts 34 | */ 35 | function isTagComment(document: TextDocument, startPosition: Position): boolean { 36 | const docIsScript: boolean = (isCfcFile(document) && hasComponent(document.uri) && getComponent(document.uri).isScript); 37 | 38 | return !docIsScript && !isInCfScript(document, startPosition); 39 | } 40 | 41 | /** 42 | * Returns the command for the comment type specified 43 | * @param commentType The comment type for which to get the command 44 | */ 45 | function getCommentCommand(commentType: CommentType): string { 46 | let command: string = ""; 47 | if (commentType === CommentType.Line) { 48 | command = "editor.action.commentLine"; 49 | } else { 50 | command = "editor.action.blockComment"; 51 | } 52 | 53 | return command; 54 | } 55 | 56 | /** 57 | * Return a function that can be used to execute a line or block comment 58 | * @param commentType The comment type for which the command will be executed 59 | */ 60 | export function toggleComment(commentType: CommentType): (editor: TextEditor) => Promise { 61 | return async (editor: TextEditor) => { 62 | if (editor) { 63 | // default comment config 64 | let languageConfig: LanguageConfiguration = { 65 | comments: { 66 | lineComment: cfmlCommentRules.scriptLineComment, 67 | blockComment: cfmlCommentRules.scriptBlockComment 68 | } 69 | }; 70 | 71 | // Changes the comment in language configuration based on the context 72 | if (isTagComment(editor.document, editor.selection.start)) { 73 | languageConfig = { 74 | comments: { 75 | blockComment: cfmlCommentRules.tagBlockComment 76 | } 77 | }; 78 | } 79 | languages.setLanguageConfiguration(LANGUAGE_ID, languageConfig); 80 | const command: string = getCommentCommand(commentType); 81 | commands.executeCommand(command); 82 | } else { 83 | window.showInformationMessage("No editor is active"); 84 | } 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /src/entities/attribute.ts: -------------------------------------------------------------------------------- 1 | import { Range, TextDocument } from "vscode"; 2 | import { MyMap, MySet, NameWithOptionalValue } from "../utils/collections"; 3 | 4 | export const ATTRIBUTES_PATTERN = /\b([\w:-]+)\b(?:(\s*(?:=|:)\s*)(?:(['"])(.*?)\3|([\w$:.]+)))?/gi; 5 | export const VALUE_PATTERN = /\b([\w:-]+)\s*(?:=|:)\s*(?:(['"])?((?:(?!\2).)*)|([\S]*))$/; 6 | 7 | export interface Attribute { 8 | name: string; // lowercased 9 | value: string; 10 | // range: Range; 11 | valueRange: Range; 12 | } 13 | 14 | export enum IncludeAttributesSetType { 15 | None = "none", 16 | Required = "required", 17 | All = "all" 18 | } 19 | 20 | // Extends Quote from textUtils.ts 21 | export enum AttributeQuoteType { 22 | None = "none", 23 | Double = "double", 24 | Single = "single" 25 | } 26 | 27 | export interface IncludeAttributesCustom { 28 | [name: string]: NameWithOptionalValue[]; // lowercased name 29 | } 30 | 31 | // Collection of attributes. Key is attribute name lowercased 32 | export class Attributes extends MyMap { } 33 | 34 | /** 35 | * Gets a regular expression that matches an attribute with the given name 36 | * @param attributeName The attribute name to use for the pattern 37 | */ 38 | export function getAttributePattern(attributeName: string): RegExp { 39 | return new RegExp(`\\b${attributeName}\\s*=\\s*(?:['"])?`, "i"); 40 | } 41 | 42 | /** 43 | * Parses a given attribute string and returns an object representation 44 | * @param document A text document containing attributes 45 | * @param attributeRange A range in which the attributes are found 46 | * @param validAttributeNames A set of valid names 47 | */ 48 | export function parseAttributes(document: TextDocument, attributeRange: Range, validAttributeNames?: MySet): Attributes { 49 | let attributeStr: string = document.getText(attributeRange); 50 | let attributes: Attributes = new Attributes(); 51 | let attributeMatch: RegExpExecArray = null; 52 | while (attributeMatch = ATTRIBUTES_PATTERN.exec(attributeStr)) { 53 | const attributeName = attributeMatch[1]; 54 | if (validAttributeNames && !validAttributeNames.has(attributeName.toLowerCase())) { 55 | continue; 56 | } 57 | const separator: string = attributeMatch[2]; 58 | const quotedValue: string = attributeMatch[4]; 59 | const unquotedValue:string = attributeMatch[5]; 60 | const attributeValue: string = quotedValue !== undefined ? quotedValue : unquotedValue; 61 | 62 | let attributeValueOffset: number; 63 | let attributeValueRange: Range; 64 | if (attributeValue) { 65 | attributeValueOffset = document.offsetAt(attributeRange.start) + attributeMatch.index + attributeName.length 66 | + separator.length + (quotedValue !== undefined ? 1 : 0); 67 | attributeValueRange = new Range( 68 | document.positionAt(attributeValueOffset), 69 | document.positionAt(attributeValueOffset + attributeValue.length) 70 | ); 71 | } 72 | 73 | attributes.set(attributeName.toLowerCase(), { 74 | name: attributeName, 75 | value: attributeValue, 76 | valueRange: attributeValueRange 77 | }); 78 | } 79 | 80 | return attributes; 81 | } 82 | -------------------------------------------------------------------------------- /src/features/commands.ts: -------------------------------------------------------------------------------- 1 | import { commands, TextDocument, Uri, window, workspace, WorkspaceConfiguration, TextEditor } from "vscode"; 2 | import { Component, getApplicationUri } from "../entities/component"; 3 | import { UserFunction } from "../entities/userFunction"; 4 | import CFDocsService from "../utils/cfdocs/cfDocsService"; 5 | import { isCfcFile } from "../utils/contextUtil"; 6 | import * as cachedEntity from "./cachedEntities"; 7 | 8 | /** 9 | * Refreshes (clears and retrieves) all CFML global definitions 10 | */ 11 | export async function refreshGlobalDefinitionCache(): Promise { 12 | cachedEntity.clearAllGlobalFunctions(); 13 | cachedEntity.clearAllGlobalTags(); 14 | cachedEntity.clearAllGlobalEntityDefinitions(); 15 | 16 | const cfmlGlobalDefinitionsSettings: WorkspaceConfiguration = workspace.getConfiguration("cfml.globalDefinitions"); 17 | if (cfmlGlobalDefinitionsSettings.get("source") === "cfdocs") { 18 | CFDocsService.cacheAll(); 19 | } 20 | } 21 | 22 | /** 23 | * Refreshes (clears and retrieves) all CFML workspace definitions 24 | */ 25 | export async function refreshWorkspaceDefinitionCache(): Promise { 26 | const cfmlIndexComponentsSettings: WorkspaceConfiguration = workspace.getConfiguration("cfml.indexComponents"); 27 | if (cfmlIndexComponentsSettings.get("enable")) { 28 | cachedEntity.cacheAllComponents(); 29 | } 30 | } 31 | 32 | /** 33 | * Opens the relevant Application file based on the given editor 34 | * @editor The text editor which represents the document for which to open the file 35 | */ 36 | export async function showApplicationDocument(editor: TextEditor): Promise { 37 | const activeDocumentUri: Uri = editor.document.uri; 38 | 39 | if (activeDocumentUri.scheme === "untitled") { 40 | return; 41 | } 42 | 43 | const applicationUri: Uri = getApplicationUri(activeDocumentUri); 44 | if (applicationUri) { 45 | const applicationDocument: TextDocument = await workspace.openTextDocument(applicationUri); 46 | if (!applicationDocument) { 47 | window.showErrorMessage("No Application found for the currently active document."); 48 | return; 49 | } 50 | 51 | window.showTextDocument(applicationDocument); 52 | } 53 | } 54 | 55 | /** 56 | * Folds all functions in the active editor. Currently only works for components. 57 | * @editor The text editor which represents the document for which to fold all function 58 | */ 59 | export async function foldAllFunctions(editor: TextEditor): Promise { 60 | const document: TextDocument = editor.document; 61 | 62 | if (isCfcFile(document)) { 63 | const thisComponent: Component = cachedEntity.getComponent(document.uri); 64 | if (thisComponent) { 65 | const functionStartLines: number[] = []; 66 | thisComponent.functions.filter((func: UserFunction) => { 67 | return !func.isImplicit && func.bodyRange !== undefined; 68 | }).forEach((func: UserFunction) => { 69 | functionStartLines.push(func.bodyRange.start.line); 70 | }); 71 | 72 | if (functionStartLines.length > 0) { 73 | commands.executeCommand("editor.fold", { selectionLines: functionStartLines }); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/entities/docblock.ts: -------------------------------------------------------------------------------- 1 | import { Range, TextDocument } from "vscode"; 2 | 3 | // If the key has no value, the last letter is ignored 4 | const DOC_PATTERN: RegExp = /(\n\s*(?:\*[ \t]*)?(?:@(\w+)(?:\.(\w+))?)?[ \t]*)(\S.*)/gi; 5 | 6 | export interface DocBlockKeyValue { 7 | key: string; // lowercased 8 | subkey?: string; // lowercased 9 | value: string; 10 | valueRange?: Range; 11 | } 12 | 13 | /** 14 | * Parses a CFScript documentation block and returns an array of DocBlockKeyValue objects 15 | * @param document The document in which to parse 16 | * @param docRange The range within the document containing the docblock 17 | */ 18 | export function parseDocBlock(document: TextDocument, docRange: Range): DocBlockKeyValue[] { 19 | const docBlockStr: string = document.getText(docRange); 20 | let docBlock: DocBlockKeyValue[] = []; 21 | let prevKey = "hint"; 22 | let activeKey = "hint"; 23 | let prevSubkey = undefined; 24 | let activeSubkey = undefined; 25 | let activeValue = undefined; 26 | let activeValueStartOffset = 0; 27 | let activeValueEndOffset = 0; 28 | let docBlockMatches: RegExpExecArray = null; 29 | const docBlockOffset: number = document.offsetAt(docRange.start); 30 | while (docBlockMatches = DOC_PATTERN.exec(docBlockStr)) { 31 | const valuePrefix: string = docBlockMatches[1]; 32 | const metadataKey: string = docBlockMatches[2]; 33 | const metadataSubkey: string = docBlockMatches[3]; 34 | const metadataValue: string = docBlockMatches[4]; 35 | const docValueOffset: number = docBlockOffset + docBlockMatches.index + valuePrefix.length; 36 | 37 | if (metadataKey) { 38 | activeKey = metadataKey.toLowerCase(); 39 | if (metadataSubkey) { 40 | activeSubkey = metadataSubkey.toLowerCase(); 41 | } else { 42 | activeSubkey = undefined; 43 | } 44 | } else if (metadataValue === "*") { 45 | continue; 46 | } 47 | 48 | if ((activeKey !== prevKey || activeSubkey !== prevSubkey) && activeValue) { 49 | docBlock.push({ 50 | key: prevKey, 51 | subkey: prevSubkey, 52 | value: activeValue, 53 | valueRange: new Range(document.positionAt(activeValueStartOffset), document.positionAt(activeValueEndOffset)) 54 | }); 55 | prevKey = activeKey; 56 | prevSubkey = activeSubkey; 57 | activeValue = undefined; 58 | } 59 | 60 | if (activeValue) { 61 | activeValue += " " + metadataValue; 62 | } else { 63 | activeValueStartOffset = docValueOffset; 64 | activeValue = metadataValue; 65 | } 66 | activeValueEndOffset = docValueOffset + metadataValue.length; 67 | } 68 | 69 | if (activeValue) { 70 | docBlock.push({ 71 | key: activeKey, 72 | subkey: activeSubkey, 73 | value: activeValue, 74 | valueRange: new Range(document.positionAt(activeValueStartOffset), document.positionAt(activeValueEndOffset)) 75 | }); 76 | } 77 | 78 | return docBlock; 79 | } 80 | 81 | /** 82 | * Gets a regular expression that matches a docblock key with the given name and captures its next word 83 | * @param keyName The tag key to match 84 | */ 85 | export function getKeyPattern(keyName: string): RegExp { 86 | return new RegExp(`@${keyName}\\s+(\\S+)`, "i"); 87 | } 88 | -------------------------------------------------------------------------------- /src/entities/query.ts: -------------------------------------------------------------------------------- 1 | import { MySet } from "../utils/collections"; 2 | import { Variable } from "./variable"; 3 | 4 | // TODO: Get query name 5 | // const queryScriptPattern: RegExp = /((?:setSql|queryExecute)\s*\(|sql\s*=)\s*(['"])([\s\S]*?)\2\s*[),]/gi; 6 | 7 | export const queryValuePattern: RegExp = /^(?:["']\s*#\s*)?(query(?:New|Execute)?)\(/i; 8 | 9 | const selectQueryPattern: RegExp = /^\s*SELECT\s+([\s\S]+?)\s+FROM\s+[\s\S]+/i; 10 | 11 | export const queryObjectProperties = { 12 | "columnList": { 13 | detail: "(property) queryName.columnList", 14 | description: "Comma-separated list of the query columns." 15 | }, 16 | "currentRow": { 17 | detail: "(property) queryName.currentRow", 18 | description: "Current row of query that is processing within a loop." 19 | }, 20 | "recordCount": { 21 | detail: "(property) queryName.recordCount", 22 | description: "Number of records (rows) returned from the query." 23 | }, 24 | }; 25 | 26 | export const queryResultProperties = { 27 | "cached": { 28 | detail: "(property) resultName.cached", 29 | description: "True if the query was cached; False otherwise." 30 | }, 31 | "columnList": { 32 | detail: "(property) resultName.columnList", 33 | description: "Comma-separated list of the query columns." 34 | }, 35 | "executionTime": { 36 | detail: "(property) resultName.executionTime", 37 | description: "Cumulative time required to process the query." 38 | }, 39 | "generatedKey": { 40 | detail: "(property) resultName.generatedKey", 41 | description: "Supports all databases. The ID of an inserted row." 42 | }, 43 | "recordCount": { 44 | detail: "(property) resultName.recordCount", 45 | description: "Number of records (rows) returned from the query." 46 | }, 47 | "sql": { 48 | detail: "(property) resultName.sql", 49 | description: "The SQL statement that was executed." 50 | }, 51 | "sqlParameters": { 52 | detail: "(property) resultName.sqlParameters", 53 | description: "An ordered Array of cfqueryparam values." 54 | }, 55 | }; 56 | 57 | export interface Query extends Variable { 58 | selectColumnNames: QueryColumns; 59 | } 60 | 61 | export class QueryColumns extends MySet { } 62 | 63 | export function getSelectColumnsFromQueryText(sql: string): QueryColumns { 64 | let selectColumnNames: QueryColumns = new MySet(); 65 | 66 | if (sql) { 67 | const selectQueryMatch: RegExpMatchArray = sql.match(selectQueryPattern); 68 | 69 | if (selectQueryMatch) { 70 | const columns: string = selectQueryMatch[1]; 71 | columns.replace(/[\[\]"`]/g, "").split(",").forEach((column: string) => { 72 | const splitColumn: string[] = column.trim().split(/[\s.]+/); 73 | if (splitColumn.length > 0) { 74 | const columnName = splitColumn.pop(); 75 | if (columnName !== "*") { 76 | selectColumnNames.add(columnName); 77 | } 78 | } 79 | }); 80 | } 81 | } 82 | 83 | return selectColumnNames; 84 | } 85 | 86 | /** 87 | * Checks whether a Variable is a Query 88 | * @param variable The variable object to check 89 | */ 90 | export function isQuery(variable: Variable): variable is Query { 91 | return "selectColumnNames" in variable; 92 | } 93 | -------------------------------------------------------------------------------- /snippets/snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": { 3 | "prefix": "component …", 4 | "body": [ 5 | "$2", 6 | "component$1 {", 7 | "\t$0", 8 | "}" 9 | ], 10 | "description": "Component definition" 11 | }, 12 | "function": { 13 | "prefix": "function …", 14 | "body": [ 15 | "$0", 16 | "${1|access,public,package,private,remote|} ${2|returnType,any,array,binary,boolean,component,date,function,guid,numeric,query,string,struct,uuid,variableName,void,xml|} function ${3:name}($4) {", 17 | "\t$5", 18 | "}" 19 | ], 20 | "description": "Function definition" 21 | }, 22 | "argument": { 23 | "prefix": "arg …", 24 | "body": "${1:required }${2|any,array,binary,boolean,component,date,function,guid,numeric,query,string,struct,uuid,variableName,void,xml|} ${3:name}$0", 25 | "description": "Argument" 26 | }, 27 | "switch": { 28 | "prefix": "switch …", 29 | "body": [ 30 | "switch (${1:expression}) {", 31 | "\tcase ${2:value}:", 32 | "\t\t${3}", 33 | "\t\tbreak;${4}", 34 | "\tdefault:", 35 | "\t\t${5}", 36 | "}$0" 37 | ], 38 | "description": "Switch block" 39 | }, 40 | "case": { 41 | "prefix": "case …", 42 | "body": [ 43 | "case ${1:value}:", 44 | "\t${2}", 45 | "\tbreak;$0" 46 | ], 47 | "description": "Case block" 48 | }, 49 | "dowhile": { 50 | "prefix": "do while …", 51 | "body": [ 52 | "do {", 53 | "\t${1}", 54 | "} while (${2:condition});$0" 55 | ], 56 | "description": "Do-While loop" 57 | }, 58 | "while": { 59 | "prefix": "while …", 60 | "body": [ 61 | "while (${1:condition}) {", 62 | "\t${2}", 63 | "}$0" 64 | ], 65 | "description": "While loop" 66 | }, 67 | "if": { 68 | "prefix": "if …", 69 | "body": [ 70 | "if (${1:condition}) {", 71 | "\t${2}", 72 | "}$0" 73 | ], 74 | "description": "If block" 75 | }, 76 | "if else": { 77 | "prefix": "ifelse …", 78 | "body": [ 79 | "if (${1:condition}) {", 80 | "\t${2}", 81 | "} else {", 82 | "\t${3}", 83 | "}$0" 84 | ], 85 | "description": "If-Else block" 86 | }, 87 | "else": { 88 | "prefix": "else …", 89 | "body": [ 90 | "else {", 91 | "\t${1}", 92 | "}$0" 93 | ], 94 | "description": "Else block" 95 | }, 96 | "elseif": { 97 | "prefix": "elseif …", 98 | "body": [ 99 | "else if (${1:condition}) {", 100 | "\t${2}", 101 | "}$0" 102 | ], 103 | "description": "Else-if block" 104 | }, 105 | "for": { 106 | "prefix": "for …", 107 | "body": [ 108 | "for (${1:i} = ${2:1}; ${1:i} < $3; ${1:i}++) {", 109 | "\t${4}", 110 | "}$0" 111 | ], 112 | "description": "For loop" 113 | }, 114 | "foreach": { 115 | "prefix": "foreach …", 116 | "body": [ 117 | "for (${1:variable} in ${2:collection}) {", 118 | "\t${3}", 119 | "}$0" 120 | ], 121 | "description": "For-each loop" 122 | }, 123 | "trycatch": { 124 | "prefix": "try …", 125 | "body": [ 126 | "try {", 127 | "\t${1}", 128 | "} catch (${2:exType} ${3:exName}) {", 129 | "\t${4}", 130 | "}$0" 131 | ], 132 | "description": "Try-catch block" 133 | }, 134 | "trycatchfinally": { 135 | "prefix": "tryfinally …", 136 | "body": [ 137 | "try {", 138 | "\t${1}", 139 | "} catch (${2:exType} ${3:exName}) {", 140 | "\t${4}", 141 | "} finally {", 142 | "\t${5}", 143 | "}$0" 144 | ], 145 | "description": "Try-catch-finally block" 146 | }, 147 | "catch": { 148 | "prefix": "catch …", 149 | "body": [ 150 | "catch (${2:exType} ${3:exName}) {", 151 | "\t${4}", 152 | "}$0" 153 | ], 154 | "description": "Catch block" 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/entities/css/cssLanguageTypes.ts: -------------------------------------------------------------------------------- 1 | /** Adopted from https://github.com/Microsoft/vscode-css-languageservice/blob/27f369f0d527b1952689e223960f779e89457374/src/cssLanguageTypes.ts */ 2 | 3 | import { Range, Position } from "vscode"; 4 | 5 | export interface PropertyCompletionContext { 6 | propertyName: string; 7 | range: Range; 8 | } 9 | 10 | export interface PropertyValueCompletionContext { 11 | propertyName: string; 12 | propertyValue?: string; 13 | range: Range; 14 | } 15 | 16 | export interface URILiteralCompletionContext { 17 | uriValue: string; 18 | position: Position; 19 | range: Range; 20 | } 21 | 22 | export interface ImportPathCompletionContext { 23 | pathValue: string; 24 | position: Position; 25 | range: Range; 26 | } 27 | 28 | export interface ICompletionParticipant { 29 | onCssProperty?: (context: PropertyCompletionContext) => void; 30 | onCssPropertyValue?: (context: PropertyValueCompletionContext) => void; 31 | onCssURILiteralValue?: (context: URILiteralCompletionContext) => void; 32 | onCssImportPath?: (context: ImportPathCompletionContext) => void; 33 | } 34 | 35 | export interface DocumentContext { 36 | resolveReference(ref: string, base?: string): string; 37 | } 38 | 39 | export interface LanguageServiceOptions { 40 | customDataProviders?: ICSSDataProvider[]; 41 | } 42 | 43 | export type EntryStatus = "standard" | "experimental" | "nonstandard" | "obsolete"; 44 | 45 | export interface IPropertyData { 46 | name: string; 47 | description?: string; 48 | browsers?: string[]; 49 | restrictions?: string[]; 50 | status?: EntryStatus; 51 | syntax?: string; 52 | values?: IValueData[]; 53 | } 54 | export interface IAtDirectiveData { 55 | name: string; 56 | description?: string; 57 | browsers?: string[]; 58 | status?: EntryStatus; 59 | } 60 | export interface IPseudoClassData { 61 | name: string; 62 | description?: string; 63 | browsers?: string[]; 64 | status?: EntryStatus; 65 | } 66 | export interface IPseudoElementData { 67 | name: string; 68 | description?: string; 69 | browsers?: string[]; 70 | status?: EntryStatus; 71 | } 72 | 73 | export interface IValueData { 74 | name: string; 75 | description?: string; 76 | browsers?: string[]; 77 | status?: EntryStatus; 78 | } 79 | 80 | export interface CSSDataV1 { 81 | version: 1; 82 | properties?: IPropertyData[]; 83 | atDirectives?: IAtDirectiveData[]; 84 | pseudoClasses?: IPseudoClassData[]; 85 | pseudoElements?: IPseudoElementData[]; 86 | } 87 | 88 | export interface ICSSDataProvider { 89 | provideProperties(): IPropertyData[]; 90 | provideAtDirectives(): IAtDirectiveData[]; 91 | providePseudoClasses(): IPseudoClassData[]; 92 | providePseudoElements(): IPseudoElementData[]; 93 | } 94 | 95 | export interface ICSSDataManager { 96 | addDataProviders(providers: ICSSDataProvider[]): void; 97 | getProperty(name: string): IPropertyData; 98 | getAtDirective(name: string): IAtDirectiveData; 99 | getPseudoClass(name: string): IPseudoClassData; 100 | getPseudoElement(name: string): IPseudoElementData; 101 | getProperties(majorBrowserSupport?: boolean): IPropertyData[]; 102 | getAtDirectives(majorBrowserSupport?: boolean): IAtDirectiveData[]; 103 | getPseudoClasses(majorBrowserSupport?: boolean): IPseudoClassData[]; 104 | getPseudoElements(majorBrowserSupport?: boolean): IPseudoElementData[]; 105 | isKnownProperty(name: string): boolean; 106 | isStandardProperty(name: string): boolean; 107 | } 108 | 109 | export type IEntry = IPropertyData | IAtDirectiveData | IPseudoClassData | IPseudoElementData | IValueData; 110 | -------------------------------------------------------------------------------- /src/entities/function.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from "./dataType"; 2 | import { Signature, constructSignatureLabelParamsPart, constructSignatureLabelParamsPrefix } from "./signature"; 3 | import { UserFunction } from "./userFunction"; 4 | import { COMPONENT_EXT } from "./component"; 5 | import * as path from "path"; 6 | import { DocumentStateContext } from "../utils/documentUtil"; 7 | import { Range, TextDocument, Position } from "vscode"; 8 | import { getNextCharacterPosition } from "../utils/contextUtil"; 9 | 10 | const functionSuffixPattern: RegExp = /^\s*\(([^)]*)/; 11 | 12 | export interface Function { 13 | name: string; 14 | description: string; 15 | returntype: DataType; 16 | signatures: Signature[]; 17 | } 18 | 19 | export enum MemberType { 20 | Array = "array", 21 | Date = "date", 22 | Image = "image", 23 | List = "list", 24 | Query = "query", 25 | String = "string", 26 | Struct = "struct", 27 | Spreadsheet = "spreadsheet", 28 | XML = "xml" 29 | } 30 | 31 | /** 32 | * Constructs a string showing a function invocation sample 33 | * @param func The function for which the syntax string will be constructed 34 | * @param signatureIndex The index of the signature to use 35 | */ 36 | export function constructSyntaxString(func: Function, signatureIndex: number = 0): string { 37 | const funcSignatureParamsLabel = func.signatures.length !== 0 ? constructSignatureLabelParamsPart(func.signatures[signatureIndex].parameters) : ""; 38 | const returnType: string = getReturnTypeString(func); 39 | 40 | return `${constructSignatureLabelParamsPrefix(func)}(${funcSignatureParamsLabel}): ${returnType}`; 41 | } 42 | 43 | /** 44 | * Gets a regular expression that matches after the function identifier and captures the parameter contents 45 | */ 46 | export function getFunctionSuffixPattern(): RegExp { 47 | return functionSuffixPattern; 48 | } 49 | 50 | /** 51 | * Gets a display string for the given function's return type 52 | * @param func The function for which to get the display return type 53 | */ 54 | export function getReturnTypeString(func: Function): string { 55 | let returnType: string; 56 | if ("returnTypeUri" in func) { 57 | const userFunction: UserFunction = func as UserFunction; 58 | if (userFunction.returnTypeUri) { 59 | returnType = path.basename(userFunction.returnTypeUri.fsPath, COMPONENT_EXT); 60 | } 61 | } 62 | 63 | if (!returnType) { 64 | returnType = func.returntype ? func.returntype : DataType.Any; 65 | } 66 | 67 | return returnType; 68 | } 69 | 70 | /** 71 | * Gets the ranges for each argument given the range for all of the arguments 72 | * @param documentStateContext The context information for the TextDocument containing function arguments 73 | * @param argsRange The full range for a set of arguments 74 | * @param separatorChar The character that separates function arguments 75 | */ 76 | export function getScriptFunctionArgRanges(documentStateContext: DocumentStateContext, argsRange: Range, separatorChar: string = ","): Range[] { 77 | let argRanges: Range[] = []; 78 | const document: TextDocument = documentStateContext.document; 79 | const argsEndOffset: number = document.offsetAt(argsRange.end); 80 | 81 | let argStartPosition = argsRange.start; 82 | while (argStartPosition.isBeforeOrEqual(argsRange.end)) { 83 | const argSeparatorPos: Position = getNextCharacterPosition(documentStateContext, document.offsetAt(argStartPosition), argsEndOffset, separatorChar, false); 84 | const argRange: Range = new Range(argStartPosition, argSeparatorPos); 85 | argRanges.push(argRange); 86 | argStartPosition = argSeparatorPos.translate(0, 1); 87 | } 88 | 89 | return argRanges; 90 | } -------------------------------------------------------------------------------- /src/features/documentLinkProvider.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { CancellationToken, DocumentLink, DocumentLinkProvider, Position, Range, TextDocument, Uri, workspace, WorkspaceFolder } from "vscode"; 4 | import { isUri } from "../utils/textUtil"; 5 | 6 | export default class CFMLDocumentLinkProvider implements DocumentLinkProvider { 7 | 8 | private linkPatterns: LinkPattern[] = [ 9 | // attribute/value link 10 | { 11 | pattern: /\b(href|src|template|action|url)\s*(?:=|:|\()\s*(['"])([^'"]+?)\2/gi, 12 | linkIndex: 3 13 | }, 14 | // include script 15 | { 16 | pattern: /\binclude\s+(['"])([^'"]+?)\1/gi, 17 | linkIndex: 2 18 | }, 19 | ]; 20 | 21 | /** 22 | * Provide links for the given document. 23 | * @param document The document in which the links are located. 24 | * @param _token A cancellation token. 25 | */ 26 | public async provideDocumentLinks(document: TextDocument, _token: CancellationToken): Promise { 27 | const results: DocumentLink[] = []; 28 | const documentText: string = document.getText(); 29 | 30 | let match: RegExpExecArray | null; 31 | 32 | this.linkPatterns.forEach((element: LinkPattern) => { 33 | const pattern: RegExp = element.pattern; 34 | while ((match = pattern.exec(documentText))) { 35 | const link: string = match[element.linkIndex]; 36 | const preLen: number = match[0].indexOf(link); 37 | const offset: number = (match.index || 0) + preLen; 38 | const linkStart: Position = document.positionAt(offset); 39 | const linkEnd: Position = document.positionAt(offset + link.length); 40 | try { 41 | const target: Uri = this.resolveLink(document, link); 42 | if (target) { 43 | results.push( 44 | new DocumentLink( 45 | new Range(linkStart, linkEnd), 46 | target 47 | ) 48 | ); 49 | } 50 | } catch (e) { 51 | // noop 52 | } 53 | } 54 | }); 55 | 56 | return results; 57 | } 58 | 59 | /** 60 | * Resolves given link text within a given document to a URI 61 | * @param document The document containing link text 62 | * @param link The link text to resolve 63 | */ 64 | private resolveLink(document: TextDocument, link: string): Uri | undefined { 65 | if (link.startsWith("#")) { 66 | return undefined; 67 | } 68 | 69 | // Check for URI 70 | if (isUri(link)) { 71 | try { 72 | const uri: Uri = Uri.parse(link); 73 | if (uri.scheme) { 74 | return uri; 75 | } 76 | } catch (e) { 77 | // noop 78 | } 79 | } 80 | 81 | // Check for relative local file 82 | const linkPath: string = link.split(/[?#]/)[0]; 83 | let resourcePath: string; 84 | if (linkPath && linkPath[0] === "/") { 85 | // Relative to root 86 | const root: WorkspaceFolder = workspace.getWorkspaceFolder(document.uri); 87 | if (root) { 88 | resourcePath = path.join(root.uri.fsPath, linkPath); 89 | } 90 | } else { 91 | // Relative to document location 92 | const base: string = path.dirname(document.fileName); 93 | resourcePath = path.join(base, linkPath); 94 | } 95 | 96 | // Check custom virtual directories? 97 | 98 | if (resourcePath && fs.existsSync(resourcePath) && fs.statSync(resourcePath).isFile()) { 99 | return Uri.file(resourcePath); 100 | } 101 | 102 | return undefined; 103 | } 104 | } 105 | 106 | interface LinkPattern { 107 | pattern: RegExp; 108 | linkIndex: number; 109 | } 110 | -------------------------------------------------------------------------------- /src/features/docBlocker/doc.ts: -------------------------------------------------------------------------------- 1 | import { workspace, SnippetString, Uri } from "vscode"; 2 | 3 | interface Config { 4 | gap: boolean; 5 | extra: ConfigExtra[]; 6 | } 7 | 8 | interface ConfigExtra { 9 | name: string; 10 | default: string; 11 | types: string[]; 12 | } 13 | 14 | export enum DocType { 15 | Component="component", 16 | Interface="interface", 17 | Property="property", 18 | Function="function", 19 | Unknown="unknown" 20 | } 21 | 22 | /** 23 | * Represents a comment block. 24 | * 25 | * This class collects data about the snippet then builds 26 | * it with the appropriate tags 27 | */ 28 | export class Doc { 29 | /** 30 | * List of param tags 31 | */ 32 | public params: string[] = []; 33 | 34 | /** 35 | * The message portion of the block 36 | */ 37 | public hint: string; 38 | 39 | /** 40 | * The type of structure being documented 41 | */ 42 | public docType: DocType; 43 | 44 | /** 45 | * The URI of the document in which this doc is relevant 46 | */ 47 | public uri: Uri; 48 | 49 | /** 50 | * A config which will modify the result of the Doc 51 | */ 52 | protected config: Config; 53 | 54 | /** 55 | * Creates an instance of Doc. 56 | * 57 | * @param hint 58 | */ 59 | public constructor(docType: DocType, uri: Uri) { 60 | this.docType = docType; 61 | this.hint = "Undocumented " + docType; 62 | this.uri = uri; 63 | } 64 | 65 | /** 66 | * Get the config from either vs code or the manually set one 67 | */ 68 | public getConfig(): Config { 69 | if (!this.config) { 70 | this.config = workspace.getConfiguration("cfml", this.uri).get("docBlock"); 71 | } 72 | return this.config; 73 | } 74 | 75 | /** 76 | * Set the config object 77 | * 78 | * @param config 79 | */ 80 | public setConfig(config: Config): void { 81 | this.config = config; 82 | } 83 | 84 | /** 85 | * Get the URI 86 | */ 87 | public getUri(): Uri { 88 | return this.uri; 89 | } 90 | 91 | /** 92 | * Set the URI 93 | * 94 | * @param uri 95 | */ 96 | public setUri(uri: Uri): void { 97 | this.uri = uri; 98 | } 99 | 100 | /** 101 | * Build all the set values into a SnippetString ready for use 102 | * 103 | * @param isEmpty 104 | */ 105 | public build(isEmpty: boolean = false): SnippetString { 106 | let snippet = new SnippetString(); 107 | let extra: ConfigExtra[] = this.getConfig().extra; 108 | let gap: boolean = !this.getConfig().gap; 109 | 110 | if (isEmpty) { 111 | gap = true; 112 | extra = []; 113 | } 114 | 115 | snippet.appendText("/**"); 116 | snippet.appendText("\n * "); 117 | snippet.appendPlaceholder(this.hint); 118 | 119 | if (this.params.length) { 120 | if (!gap) { 121 | snippet.appendText("\n *"); 122 | gap = true; 123 | } 124 | this.params.forEach((param) => { 125 | snippet.appendText(`\n * @${param} `); 126 | snippet.appendPlaceholder(""); 127 | }); 128 | } 129 | 130 | if (Array.isArray(extra) && extra.length > 0) { 131 | if (!gap) { 132 | snippet.appendText("\n *"); 133 | gap = true; 134 | } 135 | extra.filter((extraItem: ConfigExtra) => { 136 | if (extraItem.types && Array.isArray(extraItem.types)) { 137 | return extraItem.types.includes(this.docType); 138 | } 139 | return true; 140 | }).forEach((extra: ConfigExtra) => { 141 | snippet.appendText(`\n * @${extra.name} `); 142 | snippet.appendPlaceholder(extra.default); 143 | }); 144 | } 145 | 146 | snippet.appendText("\n */"); 147 | 148 | return snippet; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: 5 | - published 6 | workflow_dispatch: 7 | inputs: 8 | publishMS: 9 | description: 'Publish to the VS Marketplace' 10 | type: boolean 11 | required: true 12 | default: "true" 13 | publishOVSX: 14 | description: 'Publish to OpenVSX' 15 | type: boolean 16 | required: true 17 | default: "true" 18 | publishGH: 19 | description: 'Publish to GitHub Releases' 20 | type: boolean 21 | required: true 22 | default: "true" 23 | 24 | jobs: 25 | package: 26 | name: Package 27 | runs-on: ubuntu-latest 28 | outputs: 29 | packageName: ${{ steps.setup.outputs.packageName }} 30 | tag: ${{ steps.setup-tag.outputs.tag }} 31 | version: ${{ steps.setup-tag.outputs.version }} 32 | steps: 33 | - uses: actions/checkout@v2 34 | - uses: actions/setup-node@v2 35 | with: 36 | node-version: 14 37 | registry-url: https://registry.npmjs.org/ 38 | 39 | - name: Install dependencies 40 | run: npm i 41 | 42 | - name: Setup package path 43 | id: setup 44 | run: echo "::set-output name=packageName::$(node -e "console.log(require('./package.json').name + '-' + require('./package.json').version + '.vsix')")" 45 | 46 | - name: Package 47 | run: | 48 | npx vsce package --out ${{ steps.setup.outputs.packageName }} 49 | 50 | - uses: actions/upload-artifact@v2 51 | with: 52 | name: ${{ steps.setup.outputs.packageName }} 53 | path: ./${{ steps.setup.outputs.packageName }} 54 | if-no-files-found: error 55 | 56 | - name: Setup tag 57 | id: setup-tag 58 | run: | 59 | $version = (Get-Content ./package.json -Raw | ConvertFrom-Json).version 60 | Write-Host "tag: release/$version" 61 | Write-Host "::set-output name=tag::release/$version" 62 | Write-Host "::set-output name=version::$version" 63 | shell: pwsh 64 | 65 | publishMS: 66 | name: Publish to VS marketplace 67 | runs-on: ubuntu-latest 68 | needs: package 69 | if: github.event.inputs.publishMS == 'true' 70 | steps: 71 | - uses: actions/checkout@v2 72 | - uses: actions/download-artifact@v2 73 | with: 74 | name: ${{ needs.package.outputs.packageName }} 75 | - name: Publish to VS marketplace 76 | run: npx vsce publish --packagePath ./${{ needs.package.outputs.packageName }} -p ${{ secrets.VSCE_PAT }} 77 | 78 | publishOVSX: 79 | name: Publish to OpenVSX 80 | runs-on: ubuntu-latest 81 | needs: package 82 | if: github.event.inputs.publishOVSX == 'true' 83 | steps: 84 | - uses: actions/checkout@v2 85 | - uses: actions/download-artifact@v2 86 | with: 87 | name: ${{ needs.package.outputs.packageName }} 88 | - name: Publish to OpenVSX 89 | run: npx ovsx publish ./${{ needs.package.outputs.packageName }} -p ${{ secrets.OVSX_PAT }} 90 | 91 | publishGH: 92 | name: Publish to GitHub releases 93 | runs-on: ubuntu-latest 94 | needs: package 95 | if: github.event.inputs.publishGH == 'true' 96 | steps: 97 | - uses: actions/download-artifact@v2 98 | with: 99 | name: ${{ needs.package.outputs.packageName }} 100 | 101 | - name: Create Release 102 | id: create-release 103 | uses: actions/create-release@v1 104 | env: 105 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 106 | with: 107 | tag_name: ${{ needs.package.outputs.tag }} 108 | release_name: Release ${{ needs.package.outputs.version }} 109 | draft: false 110 | prerelease: false 111 | 112 | - name: Upload assets to a Release 113 | uses: AButler/upload-release-assets@v2.0 114 | with: 115 | files: ${{ needs.package.outputs.packageName }} 116 | release-tag: ${{ needs.package.outputs.tag }} 117 | repo-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /src/entities/html/htmlLanguageTypes.ts: -------------------------------------------------------------------------------- 1 | /** Adopted from https://github.com/Microsoft/vscode-html-languageservice/blob/600d3fc04a2db260df174526b5e71965c170e638/src/htmlLanguageTypes.ts */ 2 | 3 | import { TextDocument, Position, Range } from "vscode"; 4 | 5 | export interface HTMLFormatConfiguration { 6 | tabSize?: number; 7 | insertSpaces?: boolean; 8 | wrapLineLength?: number; 9 | unformatted?: string; 10 | contentUnformatted?: string; 11 | indentInnerHtml?: boolean; 12 | wrapAttributes?: "auto" | "force" | "force-aligned" | "force-expand-multiline" | "aligned-multiple" | "preserve" | "preserve-aligned"; 13 | wrapAttributesIndentSize?: number; 14 | preserveNewLines?: boolean; 15 | maxPreserveNewLines?: number; 16 | indentHandlebars?: boolean; 17 | endWithNewline?: boolean; 18 | extraLiners?: string; 19 | } 20 | 21 | export interface CompletionConfiguration { 22 | [provider: string]: boolean | undefined; 23 | hideAutoCompleteProposals?: boolean; 24 | } 25 | 26 | export interface Node { 27 | tag: string | undefined; 28 | start: number; 29 | startTagEnd: number | undefined; 30 | end: number; 31 | endTagStart: number | undefined; 32 | children: Node[]; 33 | parent?: Node; 34 | attributes?: { [name: string]: string | null } | undefined; 35 | } 36 | 37 | export enum TokenType { 38 | StartCommentTag, 39 | Comment, 40 | EndCommentTag, 41 | StartTagOpen, 42 | StartTagClose, 43 | StartTagSelfClose, 44 | StartTag, 45 | EndTagOpen, 46 | EndTagClose, 47 | EndTag, 48 | DelimiterAssign, 49 | AttributeName, 50 | AttributeValue, 51 | StartDoctypeTag, 52 | Doctype, 53 | EndDoctypeTag, 54 | Content, 55 | Whitespace, 56 | Unknown, 57 | Script, 58 | Styles, 59 | EOS 60 | } 61 | 62 | export enum ScannerState { 63 | WithinContent, 64 | AfterOpeningStartTag, 65 | AfterOpeningEndTag, 66 | WithinDoctype, 67 | WithinTag, 68 | WithinEndTag, 69 | WithinComment, 70 | WithinScriptContent, 71 | WithinStyleContent, 72 | AfterAttributeName, 73 | BeforeAttributeValue 74 | } 75 | 76 | export interface Scanner { 77 | scan(): TokenType; 78 | getTokenType(): TokenType; 79 | getTokenOffset(): number; 80 | getTokenLength(): number; 81 | getTokenEnd(): number; 82 | getTokenText(): string; 83 | getTokenError(): string | undefined; 84 | getScannerState(): ScannerState; 85 | } 86 | 87 | export declare type HTMLDocument = { 88 | roots: Node[]; 89 | findNodeBefore(offset: number): Node; 90 | findNodeAt(offset: number): Node; 91 | }; 92 | 93 | export interface DocumentContext { 94 | resolveReference(ref: string, base?: string): string; 95 | } 96 | 97 | export interface HtmlAttributeValueContext { 98 | document: TextDocument; 99 | position: Position; 100 | tag: string; 101 | attribute: string; 102 | value: string; 103 | range: Range; 104 | } 105 | 106 | export interface HtmlContentContext { 107 | document: TextDocument; 108 | position: Position; 109 | } 110 | 111 | export interface ICompletionParticipant { 112 | onHtmlAttributeValue?: (context: HtmlAttributeValueContext) => void; 113 | onHtmlContent?: (context: HtmlContentContext) => void; 114 | } 115 | 116 | export interface ITagData { 117 | name: string; 118 | description?: string; 119 | attributes: IAttributeData[]; 120 | } 121 | 122 | export interface IAttributeData { 123 | name: string; 124 | description?: string; 125 | valueSet?: string; 126 | values?: IValueData[]; 127 | } 128 | 129 | export interface IValueData { 130 | name: string; 131 | description?: string; 132 | } 133 | 134 | export interface IValueSet { 135 | name: string; 136 | values: IValueData[]; 137 | } 138 | 139 | export interface HTMLDataV1 { 140 | version: 1; 141 | tags?: ITagData[]; 142 | globalAttributes?: IAttributeData[]; 143 | valueSets?: IValueSet[]; 144 | } 145 | 146 | export interface IHTMLDataProvider { 147 | getId(): string; 148 | isApplicable(languageId: string): boolean; 149 | 150 | provideTags(): ITagData[]; 151 | provideAttributes(tag: string): IAttributeData[]; 152 | provideValues(tag: string, attribute: string): IValueData[]; 153 | } 154 | -------------------------------------------------------------------------------- /src/utils/cfdocs/multiSignatures.ts: -------------------------------------------------------------------------------- 1 | // Accommodates for the lack of proper multiple signature support in CFDocs 2 | 3 | import { MyMap } from "../collections"; 4 | 5 | export interface MinMultiSigs extends MyMap { } 6 | 7 | // TODO: Indicate version when signature was added 8 | export const multiSigGlobalFunctions: MinMultiSigs = 9 | // Key: Function name. Value: Array of signatures, consisting of array of argument names. 10 | new MyMap() 11 | /* 12 | .set("arrayFind", 13 | [ 14 | [ 15 | "array", 16 | "value" 17 | ], 18 | [ 19 | "array", 20 | "callback" 21 | ] 22 | ] 23 | ) 24 | */ 25 | .set("arraySort", 26 | [ 27 | [ 28 | "array", 29 | "sort_type", 30 | "sort_order" 31 | ], 32 | [ 33 | "array", 34 | "callback" 35 | ] 36 | ] 37 | ) 38 | .set("createObject", 39 | [ 40 | [ 41 | "type='component'", 42 | "component_name" 43 | ], 44 | [ 45 | "type='java'", 46 | "class" 47 | ], 48 | [ 49 | "type='webservice'", 50 | "urltowsdl", 51 | "portname" 52 | ], 53 | [ 54 | "type='.NET'", 55 | "class", 56 | "assembly", 57 | "server", 58 | "port", 59 | "protocol", 60 | "secure" 61 | ], 62 | [ 63 | "type='com'", 64 | "class", 65 | "context", 66 | "serverName" 67 | ] 68 | ] 69 | ) 70 | .set("isValid", 71 | [ 72 | [ 73 | "type", 74 | "value", 75 | ], 76 | [ 77 | "type='regex'", 78 | "value", 79 | "pattern" 80 | ], 81 | [ 82 | "type='range'", 83 | "value", 84 | "min", 85 | "max" 86 | ] 87 | ] 88 | ) 89 | /* 90 | .set("listSort", 91 | [ 92 | [ 93 | "array", 94 | "sort_type", 95 | "sort_order", 96 | "delimiters", 97 | "includeEmptyValues" 98 | ], 99 | [ 100 | "list", 101 | "callback" 102 | ] 103 | ] 104 | ) 105 | */ 106 | .set("replaceListNoCase", 107 | [ 108 | [ 109 | "String", 110 | "list1", 111 | "list2", 112 | "includeEmptyFields" 113 | ], 114 | [ 115 | "String", 116 | "list1", 117 | "list2", 118 | "delimiter", 119 | "includeEmptyFields" 120 | ], 121 | [ 122 | "String", 123 | "list1", 124 | "list2", 125 | "delimiterList1", 126 | "delimiterList2", 127 | "includeEmptyFields" 128 | ] 129 | ] 130 | ) 131 | /* 132 | .set("structNew", 133 | [ 134 | [ 135 | "structType", 136 | "sortType", 137 | "sortOrder", 138 | "localeSensitive" 139 | ], 140 | [ 141 | "structType", 142 | "callback" 143 | ] 144 | ] 145 | ) 146 | .set("structSort", 147 | [ 148 | [ 149 | "base", 150 | "sorttype", 151 | "sortorder", 152 | "pathtosubelement", 153 | "localeSensitive" 154 | ], 155 | [ 156 | "base", 157 | "callback" 158 | ] 159 | ] 160 | ) 161 | .set("structToSorted", 162 | [ 163 | [ 164 | "anyStruct", 165 | "sorttype", 166 | "sortorder", 167 | "localeSensitive" 168 | ], 169 | [ 170 | "anyStruct", 171 | "callback" 172 | ] 173 | ] 174 | ) 175 | .set("fileWrite", 176 | [ 177 | [ 178 | "filePath", 179 | "data", 180 | "charset" 181 | ], 182 | [ 183 | "fileObj", 184 | "data" 185 | ] 186 | ] 187 | ) 188 | */ 189 | ; 190 | -------------------------------------------------------------------------------- /src/utils/documentUtil.ts: -------------------------------------------------------------------------------- 1 | import { Position, Range, TextDocument, WorkspaceConfiguration, workspace } from "vscode"; 2 | import { Component, isScriptComponent } from "../entities/component"; 3 | import { getComponent } from "../features/cachedEntities"; 4 | import { CFMLEngine, CFMLEngineName } from "./cfdocs/cfmlEngine"; 5 | import { getCfScriptRanges, getDocumentContextRanges, isCfcFile, isCfmFile, isContinuingExpression, isInRanges, DocumentContextRanges } from "./contextUtil"; 6 | import { getSanitizedDocumentText } from "./textUtil"; 7 | 8 | export interface DocumentStateContext { 9 | document: TextDocument; 10 | isCfmFile: boolean; 11 | isCfcFile: boolean; 12 | docIsScript: boolean; 13 | commentRanges: Range[]; 14 | stringRanges?: Range[]; 15 | stringEmbeddedCfmlRanges?: Range[]; 16 | sanitizedDocumentText: string; 17 | component?: Component; 18 | userEngine: CFMLEngine; 19 | } 20 | 21 | export interface DocumentPositionStateContext extends DocumentStateContext { 22 | position: Position; 23 | positionIsScript: boolean; 24 | positionInComment: boolean; 25 | docPrefix: string; 26 | wordRange: Range; 27 | currentWord: string; 28 | isContinuingExpression: boolean; 29 | } 30 | 31 | /** 32 | * Provides context information for the given document 33 | * @param document The document for which to provide context 34 | * @param fast Whether to use the faster, but less accurate parsing 35 | */ 36 | export function getDocumentStateContext(document: TextDocument, fast: boolean = false): DocumentStateContext { 37 | const cfmlEngineSettings: WorkspaceConfiguration = workspace.getConfiguration("cfml.engine"); 38 | const userEngineName: CFMLEngineName = CFMLEngineName.valueOf(cfmlEngineSettings.get("name")); 39 | const userEngine: CFMLEngine = new CFMLEngine(userEngineName, cfmlEngineSettings.get("version")); 40 | 41 | const docIsCfmFile: boolean = isCfmFile(document); 42 | const docIsCfcFile: boolean = isCfcFile(document); 43 | const thisComponent: Component = getComponent(document.uri); 44 | const docIsScript: boolean = (docIsCfcFile && isScriptComponent(document)); 45 | const documentRanges: DocumentContextRanges = getDocumentContextRanges(document, docIsScript, undefined, fast); 46 | const commentRanges: Range[] = documentRanges.commentRanges; 47 | const stringRanges: Range[] = documentRanges.stringRanges; 48 | const stringEmbeddedCfmlRanges: Range[] = documentRanges.stringEmbeddedCfmlRanges; 49 | const sanitizedDocumentText: string = getSanitizedDocumentText(document, commentRanges); 50 | 51 | return { 52 | document, 53 | isCfmFile: docIsCfmFile, 54 | isCfcFile: docIsCfcFile, 55 | docIsScript, 56 | commentRanges, 57 | stringRanges, 58 | stringEmbeddedCfmlRanges, 59 | sanitizedDocumentText, 60 | component: thisComponent, 61 | userEngine 62 | }; 63 | } 64 | 65 | /** 66 | * Provides context information for the given document and position 67 | * @param document The document for which to provide context 68 | * @param position The position within the document for which to provide context 69 | * @param fast Whether to use the faster, but less accurate parsing 70 | */ 71 | export function getDocumentPositionStateContext(document: TextDocument, position: Position, fast: boolean = false): DocumentPositionStateContext { 72 | const documentStateContext: DocumentStateContext = getDocumentStateContext(document, fast); 73 | 74 | const docIsScript: boolean = documentStateContext.docIsScript; 75 | const positionInComment: boolean = isInRanges(documentStateContext.commentRanges, position); 76 | const cfscriptRanges: Range[] = getCfScriptRanges(document); 77 | const positionIsScript: boolean = docIsScript || isInRanges(cfscriptRanges, position); 78 | 79 | let wordRange: Range = document.getWordRangeAtPosition(position); 80 | const currentWord: string = wordRange ? document.getText(wordRange) : ""; 81 | if (!wordRange) { 82 | wordRange = new Range(position, position); 83 | } 84 | const docPrefix: string = documentStateContext.sanitizedDocumentText.slice(0, document.offsetAt(wordRange.start)); 85 | 86 | const documentPositionStateContext: DocumentPositionStateContext = Object.assign(documentStateContext, 87 | { 88 | position, 89 | positionIsScript, 90 | positionInComment, 91 | docPrefix, 92 | wordRange, 93 | currentWord, 94 | isContinuingExpression: isContinuingExpression(docPrefix) 95 | } 96 | ); 97 | 98 | return documentPositionStateContext; 99 | } 100 | -------------------------------------------------------------------------------- /src/utils/textUtil.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString, Range, TextDocument, Position } from "vscode"; 2 | import { getDocumentContextRanges, isCfcFile } from "./contextUtil"; 3 | import { getComponent, hasComponent } from "../features/cachedEntities"; 4 | import { AttributeQuoteType } from "../entities/attribute"; 5 | 6 | export enum Quote { 7 | Single = "single", 8 | Double = "double" 9 | } 10 | 11 | /** 12 | * Returns the quote of the given type 13 | */ 14 | export function getQuote(quote: Quote | AttributeQuoteType): string { 15 | let quoteStr: string = ""; 16 | 17 | switch (quote) { 18 | case Quote.Single: 19 | quoteStr = "'"; 20 | break; 21 | case Quote.Double: 22 | quoteStr = '"'; 23 | break; 24 | default: 25 | break; 26 | } 27 | 28 | return quoteStr; 29 | } 30 | 31 | /** 32 | * Returns whether the strings are equal ignoring case 33 | * @param string1 A string to compare 34 | * @param string2 A string to compare 35 | */ 36 | export function equalsIgnoreCase(string1: string, string2: string): boolean { 37 | if (string1 === undefined || string2 === undefined) { 38 | return false; 39 | } 40 | return string1.toLowerCase() === string2.toLowerCase(); 41 | } 42 | 43 | /** 44 | * Transforms text to Markdown-compatible string 45 | * @param text A candidate string 46 | */ 47 | export function textToMarkdownCompatibleString(text: string): string { 48 | return text.replace(/\n(?!\n)/g, " \n"); 49 | } 50 | 51 | /** 52 | * Transforms text to MarkdownString 53 | * @param text A candidate string 54 | */ 55 | export function textToMarkdownString(text: string): MarkdownString { 56 | return new MarkdownString(textToMarkdownCompatibleString(text)); 57 | } 58 | 59 | /** 60 | * Escapes special markdown characters 61 | * @param text A candidate string 62 | */ 63 | export function escapeMarkdown(text: string): string { 64 | return text.replace(/[\\`*_{}[\]()#+\-.!]/g, "\\$&"); 65 | } 66 | 67 | /** 68 | * Returns a text document's text with all non-whitespace characters within a given range replaced with spaces 69 | * @param document The text document in which to replace 70 | * @param range The range within which to replace text 71 | */ 72 | export function replaceRangeWithSpaces(document: TextDocument, ranges: Range[]): string { 73 | let documentText: string = document.getText(); 74 | 75 | ranges.forEach((range: Range) => { 76 | const rangeStartOffset: number = document.offsetAt(range.start); 77 | const rangeEndOffset: number = document.offsetAt(range.end); 78 | documentText = documentText.substr(0, rangeStartOffset) 79 | + documentText.substring(rangeStartOffset, rangeEndOffset).replace(/\S/g, " ") 80 | + documentText.substr(rangeEndOffset, documentText.length - rangeEndOffset); 81 | }); 82 | 83 | return documentText; 84 | } 85 | 86 | /** 87 | * Returns a text document's text replacing all comment text with whitespace. 88 | * @param document The text document from which to get text 89 | * @param commentRanges Optional ranges in which there are CFML comments 90 | */ 91 | export function getSanitizedDocumentText(document: TextDocument, commentRanges?: Range[]): string { 92 | let documentCommentRanges: Range[]; 93 | if (commentRanges) { 94 | documentCommentRanges = commentRanges; 95 | } else { 96 | const docIsScript: boolean = (isCfcFile(document) && hasComponent(document.uri) && getComponent(document.uri).isScript); 97 | documentCommentRanges = getDocumentContextRanges(document, docIsScript).commentRanges; 98 | } 99 | 100 | return replaceRangeWithSpaces(document, documentCommentRanges); 101 | } 102 | 103 | /** 104 | * Returns a text document's text before given position. Optionally replaces all comment text with whitespace. 105 | * @param document The text document in which to replace 106 | * @param position The position that marks the end of the document's text to return 107 | * @param replaceComments Whether the text should have comments replaced 108 | */ 109 | export function getPrefixText(document: TextDocument, position: Position, replaceComments: boolean = false): string { 110 | let documentText: string = document.getText(); 111 | if (replaceComments) { 112 | documentText = getSanitizedDocumentText(document); 113 | } 114 | 115 | return documentText.slice(0, document.offsetAt(position)); 116 | } 117 | 118 | 119 | // RFC 2396, Appendix A: https://www.ietf.org/rfc/rfc2396.txt 120 | const schemePattern = /^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/; 121 | 122 | /** 123 | * A valid uri starts with a scheme and the scheme has at least 2 characters so that it doesn't look like a drive letter. 124 | * @param str The candidate URI to check 125 | */ 126 | export function isUri(str: string): boolean { 127 | return str && schemePattern.test(str); 128 | } 129 | -------------------------------------------------------------------------------- /src/entities/cgi.ts: -------------------------------------------------------------------------------- 1 | interface CGIDetails { 2 | detail: string; 3 | description: string; 4 | links: string[]; 5 | } 6 | 7 | interface CGI { 8 | [cgi: string]: CGIDetails; 9 | } 10 | 11 | export const cgiVariables: CGI = { 12 | // Server 13 | "SERVER_SOFTWARE": { 14 | detail: "CGI.SERVER_SOFTWARE", 15 | description: "Name and version of the information server software answering the request (and running the gateway). Format: name/version.", 16 | links: [] 17 | }, 18 | "SERVER_NAME": { 19 | detail: "CGI.SERVER_NAME", 20 | description: "Server's hostname, DNS alias, or IP address as it appears in self-referencing URLs.", 21 | links: [] 22 | }, 23 | "GATEWAY_INTERFACE": { 24 | detail: "CGI.GATEWAY_INTERFACE", 25 | description: "CGI specification revision with which this server complies. Format: CGI/revision.", 26 | links: [] 27 | }, 28 | "SERVER_PROTOCOL": { 29 | detail: "CGI.SERVER_PROTOCOL", 30 | description: "Name and revision of the information protocol this request came in with. Format: protocol/revision.", 31 | links: [] 32 | }, 33 | "SERVER_PORT": { 34 | detail: "CGI.SERVER_PORT", 35 | description: "Port number to which the request was sent.", 36 | links: [] 37 | }, 38 | "REQUEST_METHOD": { 39 | detail: "CGI.REQUEST_METHOD", 40 | description: "Method with which the request was made. For HTTP, this is Get, Head, Post, and so on.", 41 | links: [] 42 | }, 43 | "PATH_INFO": { 44 | detail: "CGI.PATH_INFO", 45 | description: "Extra path information, as given by the client. Scripts can be accessed by their virtual pathname, followed by extra information at the end of this path. The extra information is sent as PATH_INFO.", 46 | links: [] 47 | }, 48 | "PATH_TRANSLATED": { 49 | detail: "CGI.PATH_TRANSLATED", 50 | description: "Translated version of PATH_INFO after any virtual-to-physical mapping.", 51 | links: [] 52 | }, 53 | "SCRIPT_NAME": { 54 | detail: "CGI.SCRIPT_NAME", 55 | description: "Virtual path to the script that is executing; used for self-referencing URLs.", 56 | links: [] 57 | }, 58 | "QUERY_STRING": { 59 | detail: "CGI.QUERY_STRING", 60 | description: "Query information that follows the ? in the URL that referenced this script.", 61 | links: [] 62 | }, 63 | "REMOTE_HOST": { 64 | detail: "CGI.REMOTE_HOST", 65 | description: "Hostname making the request. If the server does not have this information, it sets REMOTE_ADDR and does not set REMOTE_HOST.", 66 | links: [] 67 | }, 68 | "REMOTE_ADDR": { 69 | detail: "CGI.REMOTE_ADDR", 70 | description: "IP address of the remote host making the request.", 71 | links: [] 72 | }, 73 | "AUTH_TYPE": { 74 | detail: "CGI.AUTH_TYPE", 75 | description: "If the server supports user authentication, and the script is protected, the protocol-specific authentication method used to validate the user.", 76 | links: [] 77 | }, 78 | "REMOTE_USER": { 79 | detail: "CGI.REMOTE_USER", 80 | description: "If the server supports user authentication, and the script is protected, the username the user has authenticated as. (Also available as AUTH_USER.)", 81 | links: [] 82 | }, 83 | "AUTH_USER": { 84 | detail: "CGI.AUTH_USER", 85 | description: "If the server supports user authentication, and the script is protected, the username the user has authenticated as. (Also available as AUTH_USER.)", 86 | links: [] 87 | }, 88 | "REMOTE_IDENT": { 89 | detail: "CGI.REMOTE_IDENT", 90 | description: "If the HTTP server supports RFC 931 identification, this variable is set to the remote username retrieved from the server. Use this variable for logging only.", 91 | links: [] 92 | }, 93 | "CONTENT_TYPE": { 94 | detail: "CGI.CONTENT_TYPE", 95 | description: "For queries that have attached information, such as HTTP POST and PUT, this is the content type of the data.", 96 | links: [] 97 | }, 98 | "CONTENT_LENGTH": { 99 | detail: "CGI.CONTENT_LENGTH", 100 | description: "Length of the content as given by the client.", 101 | links: [] 102 | }, 103 | // Client 104 | "HTTP_REFERER": { 105 | detail: "CGI.HTTP_REFERER", 106 | description: "The referring document that linked to or submitted form data.", 107 | links: [] 108 | }, 109 | "HTTP_USER_AGENT": { 110 | detail: "CGI.HTTP_USER_AGENT", 111 | description: "The browser that the client is currently using to send the request. Format: software/version library/version.", 112 | links: [] 113 | }, 114 | "HTTP_IF_MODIFIED_SINCE": { 115 | detail: "CGI.HTTP_IF_MODIFIED_SINCE", 116 | description: "The last time the page was modified. The browser determines whether to set this variable, usually in response to the server having sent the LAST_MODIFIED HTTP header. It can be used to take advantage of browser-side caching.", 117 | links: [] 118 | }, 119 | "HTTP_URL": { 120 | detail: "CGI.HTTP_URL", 121 | description: "The URL path in an encoded format.", 122 | links: [] 123 | }, 124 | }; 125 | -------------------------------------------------------------------------------- /src/utils/fileUtil.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { COMPONENT_EXT } from "../entities/component"; 4 | import { equalsIgnoreCase } from "./textUtil"; 5 | import { Uri, workspace, WorkspaceFolder } from "vscode"; 6 | 7 | export interface CFMLMapping { 8 | logicalPath: string; 9 | directoryPath: string; 10 | isPhysicalDirectoryPath?: boolean; 11 | } 12 | 13 | export function getDirectories(srcPath: string): string[] { 14 | const files: string[] = fs.readdirSync(srcPath); 15 | 16 | return filterDirectories(files, srcPath); 17 | } 18 | 19 | /** 20 | * Takes an array of files and filters them to only the directories 21 | * @param files A list of files to filter 22 | * @param srcPath The path of the directory in which the files are contained 23 | */ 24 | export function filterDirectories(files: string[], srcPath: string): string[] { 25 | return files.filter((file: string) => { 26 | return fs.statSync(path.join(srcPath, file)).isDirectory(); 27 | }); 28 | } 29 | 30 | export function getComponents(srcPath: string): string[] { 31 | const files: string[] = fs.readdirSync(srcPath); 32 | 33 | return filterComponents(files); 34 | } 35 | 36 | /** 37 | * Takes an array of files and filters them to only the components 38 | * @param files A list of files to filter 39 | */ 40 | export function filterComponents(files: string[]): string[] { 41 | return files.filter((file: string) => { 42 | return equalsIgnoreCase(path.extname(file), COMPONENT_EXT); 43 | }); 44 | } 45 | 46 | /** 47 | * Resolves a dot path to a list of file paths 48 | * @param dotPath A string for a component in dot-path notation 49 | * @param baseUri The URI from which the component path will be resolved 50 | */ 51 | export function resolveDottedPaths(dotPath: string, baseUri: Uri): string[] { 52 | let paths: string[] = []; 53 | 54 | const normalizedPath: string = dotPath.replace(/\./g, path.sep); 55 | 56 | // TODO: Check imports 57 | 58 | // relative to local directory 59 | const localPath: string = resolveRelativePath(baseUri, normalizedPath); 60 | if (fs.existsSync(localPath)) { 61 | paths.push(localPath); 62 | 63 | if (normalizedPath.length > 0) { 64 | return paths; 65 | } 66 | } 67 | 68 | // relative to web root 69 | const rootPath: string = resolveRootPath(baseUri, normalizedPath); 70 | if (rootPath && fs.existsSync(rootPath)) { 71 | paths.push(rootPath); 72 | 73 | if (normalizedPath.length > 0) { 74 | return paths; 75 | } 76 | } 77 | 78 | // custom mappings 79 | const customMappingPaths: string[] = resolveCustomMappingPaths(baseUri, normalizedPath); 80 | for (const mappedPath of customMappingPaths) { 81 | if (fs.existsSync(mappedPath)) { 82 | paths.push(mappedPath); 83 | 84 | if (normalizedPath.length > 0) { 85 | return paths; 86 | } 87 | } 88 | } 89 | 90 | return paths; 91 | } 92 | 93 | /** 94 | * Resolves a full path relative to the given URI 95 | * @param baseUri The URI from which the relative path will be resolved 96 | * @param appendingPath A path appended to the given URI 97 | */ 98 | export function resolveRelativePath(baseUri: Uri, appendingPath: string): string { 99 | return path.join(path.dirname(baseUri.fsPath), appendingPath); 100 | } 101 | 102 | /** 103 | * Resolves a full path relative to the root of the given URI, or undefined if not in workspace 104 | * @param baseUri The URI from which the root path will be resolved 105 | * @param appendingPath A path appended to the resolved root path 106 | */ 107 | export function resolveRootPath(baseUri: Uri, appendingPath: string): string | undefined { 108 | const root: WorkspaceFolder = workspace.getWorkspaceFolder(baseUri); 109 | 110 | // When baseUri is not in workspace 111 | if (!root) { 112 | return undefined; 113 | } 114 | 115 | return path.join(root.uri.fsPath, appendingPath); 116 | } 117 | 118 | /** 119 | * Resolves a full path based on mappings 120 | * @param baseUri The URI from which the root path will be resolved 121 | * @param appendingPath A path appended to the resolved path 122 | */ 123 | export function resolveCustomMappingPaths(baseUri: Uri, appendingPath: string): string[] { 124 | const customMappingPaths: string[] = []; 125 | 126 | const cfmlMappings: CFMLMapping[] = workspace.getConfiguration("cfml", baseUri).get("mappings", []); 127 | const normalizedPath: string = appendingPath.replace(/\\/g, "/"); 128 | for (const cfmlMapping of cfmlMappings) { 129 | const slicedLogicalPath: string = cfmlMapping.logicalPath.slice(1); 130 | const logicalPathStartPattern = new RegExp(`^${slicedLogicalPath}(?:\/|$)`); 131 | if (logicalPathStartPattern.test(normalizedPath)) { 132 | const directoryPath: string = cfmlMapping.isPhysicalDirectoryPath === undefined || cfmlMapping.isPhysicalDirectoryPath ? cfmlMapping.directoryPath : resolveRootPath(baseUri, cfmlMapping.directoryPath); 133 | const mappedPath: string = path.join(directoryPath, appendingPath.slice(slicedLogicalPath.length)); 134 | customMappingPaths.push(mappedPath); 135 | } 136 | } 137 | 138 | return customMappingPaths; 139 | } 140 | -------------------------------------------------------------------------------- /src/utils/cfdocs/cfmlEngine.ts: -------------------------------------------------------------------------------- 1 | import * as semver from "semver"; 2 | import { DataType } from "../../entities/dataType"; 3 | import { Uri } from "vscode"; 4 | import { extensionContext } from "../../cfmlMain"; 5 | 6 | export enum CFMLEngineName { 7 | ColdFusion = "coldfusion", 8 | Lucee = "lucee", 9 | Railo = "railo", 10 | OpenBD = "openbd", 11 | Unknown = "unknown" 12 | } 13 | 14 | export namespace CFMLEngineName { 15 | /** 16 | * Resolves a string value of name to an enumeration member 17 | * @param name The name string to resolve 18 | */ 19 | export function valueOf(name: string): CFMLEngineName { 20 | switch (name.toLowerCase()) { 21 | case "coldfusion": 22 | return CFMLEngineName.ColdFusion; 23 | case "lucee": 24 | return CFMLEngineName.Lucee; 25 | case "railo": 26 | return CFMLEngineName.Railo; 27 | case "openbd": 28 | return CFMLEngineName.OpenBD; 29 | default: 30 | return CFMLEngineName.Unknown; 31 | } 32 | } 33 | } 34 | 35 | export class CFMLEngine { 36 | private name: CFMLEngineName; 37 | private version: string; 38 | 39 | constructor(name: CFMLEngineName, version: string | undefined) { 40 | this.name = name; 41 | if (semver.valid(version, true)) { 42 | this.version = semver.valid(version, true); 43 | } else { 44 | this.version = CFMLEngine.toSemVer(version); 45 | } 46 | } 47 | 48 | /** 49 | * Getter for CFML engine name 50 | */ 51 | public getName(): CFMLEngineName { 52 | return this.name; 53 | } 54 | 55 | /** 56 | * Getter for CFML engine version 57 | */ 58 | public getVersion(): string { 59 | return this.version; 60 | } 61 | 62 | /** 63 | * Check if this engine is equal to `other`. 64 | * @param other A CFML engine. 65 | */ 66 | public equals(other: CFMLEngine): boolean { 67 | if (this.name === CFMLEngineName.Unknown || other.name === CFMLEngineName.Unknown) { 68 | return false; 69 | } 70 | 71 | if (this.name === other.name) { 72 | if (!this.version && !other.version) { 73 | return true; 74 | } else if (!this.version || !other.version) { 75 | return false; 76 | } else { 77 | return semver.eq(this.version, other.version); 78 | } 79 | } 80 | 81 | return false; 82 | } 83 | 84 | /** 85 | * Check if this engine is older than `other`. Returns undefined if they have different name. 86 | * @param other A CFML engine. 87 | */ 88 | public isOlder(other: CFMLEngine): boolean | undefined { 89 | if (this.name === CFMLEngineName.Unknown || other.name === CFMLEngineName.Unknown || this.name !== other.name || !this.version || !other.version) { 90 | return undefined; 91 | } 92 | return semver.lt(this.version, other.version); 93 | } 94 | 95 | /** 96 | * Check if this engine is older than or equals `other`. Returns undefined if they have different name. 97 | * @param other A CFML engine. 98 | */ 99 | public isOlderOrEquals(other: CFMLEngine): boolean | undefined { 100 | if (this.name === CFMLEngineName.Unknown || other.name === CFMLEngineName.Unknown || this.name !== other.name || !this.version || !other.version) { 101 | return undefined; 102 | } 103 | return semver.lte(this.version, other.version); 104 | } 105 | 106 | /** 107 | * Check if this engine is newer than `other`. Returns undefined if they have different name. 108 | * @param other A CFML engine. 109 | */ 110 | public isNewer(other: CFMLEngine): boolean | undefined { 111 | if (this.name === CFMLEngineName.Unknown || other.name === CFMLEngineName.Unknown || this.name !== other.name || !this.version || !other.version) { 112 | return undefined; 113 | } 114 | return semver.gt(this.version, other.version); 115 | } 116 | 117 | /** 118 | * Check if this engine is newer than or equals `other`. Returns undefined if they have different name. 119 | * @param other A CFML engine. 120 | */ 121 | public isNewerOrEquals(other: CFMLEngine): boolean | undefined { 122 | if (this.name === CFMLEngineName.Unknown || other.name === CFMLEngineName.Unknown || this.name !== other.name || !this.version || !other.version) { 123 | return undefined; 124 | } 125 | return semver.gte(this.version, other.version); 126 | } 127 | 128 | /** 129 | * Returns whether this engine supports tags in script format 130 | */ 131 | public supportsScriptTags(): boolean { 132 | return ( 133 | this.name === CFMLEngineName.Unknown 134 | || (this.name === CFMLEngineName.ColdFusion && semver.gte(this.version, "11.0.0")) 135 | || this.name === CFMLEngineName.Lucee 136 | || (this.name === CFMLEngineName.Railo && semver.gte(this.version, "4.2.0")) 137 | ); 138 | } 139 | 140 | /** 141 | * Returns whether this engine supports named parameters for global functions 142 | */ 143 | public supportsGlobalFunctionNamedParams(): boolean { 144 | return ( 145 | this.name === CFMLEngineName.Unknown 146 | || (this.name === CFMLEngineName.ColdFusion && semver.gte(this.version, "2018.0.0")) 147 | || this.name === CFMLEngineName.Lucee 148 | || (this.name === CFMLEngineName.Railo && semver.gte(this.version, "3.3.0")) 149 | ); 150 | } 151 | 152 | /** 153 | * Attempts to transform versionStr into a valid semver version 154 | * @param versionStr A version string. 155 | */ 156 | public static toSemVer(versionStr: string): string | undefined { 157 | if (semver.clean(versionStr, true)) { 158 | return semver.clean(versionStr, true); 159 | } else if (DataType.isNumeric(versionStr)) { 160 | const splitVer: string[] = versionStr.split("."); 161 | while (splitVer.length < 3) { 162 | splitVer.push("0"); 163 | } 164 | const reconstructedVer = splitVer.join("."); 165 | if (semver.valid(reconstructedVer, true)) { 166 | return semver.valid(reconstructedVer, true); 167 | } 168 | } 169 | 170 | return undefined; 171 | } 172 | 173 | /** 174 | * Gets the CFML engine icon URI 175 | */ 176 | public static getIconUri(name: CFMLEngineName): Uri { 177 | return Uri.joinPath(extensionContext.extensionUri, `images/${name}.png`); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/features/documentSymbolProvider.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken, DocumentSymbolProvider, Position, Range, DocumentSymbol, SymbolKind, TextDocument } from "vscode"; 2 | import { Component } from "../entities/component"; 3 | import { Property } from "../entities/property"; 4 | import { getLocalVariables, UserFunction } from "../entities/userFunction"; 5 | import { parseVariableAssignments, usesConstantConvention, Variable } from "../entities/variable"; 6 | import { DocumentStateContext, getDocumentStateContext } from "../utils/documentUtil"; 7 | import { getComponent } from "./cachedEntities"; 8 | import { Scope } from "../entities/scope"; 9 | 10 | export default class CFMLDocumentSymbolProvider implements DocumentSymbolProvider { 11 | /** 12 | * Provide symbol information for the given document. 13 | * @param document The document for which to provide symbols. 14 | * @param _token A cancellation token. 15 | */ 16 | public async provideDocumentSymbols(document: TextDocument, _token: CancellationToken): Promise { 17 | let documentSymbols: DocumentSymbol[] = []; 18 | 19 | if (!document.fileName) { 20 | return documentSymbols; 21 | } 22 | 23 | const documentStateContext: DocumentStateContext = getDocumentStateContext(document); 24 | 25 | if (documentStateContext.isCfcFile) { 26 | documentSymbols = documentSymbols.concat(CFMLDocumentSymbolProvider.getComponentSymbols(documentStateContext)); 27 | } else if (documentStateContext.isCfmFile) { 28 | documentSymbols = documentSymbols.concat(CFMLDocumentSymbolProvider.getTemplateSymbols(documentStateContext)); 29 | } 30 | 31 | return documentSymbols; 32 | } 33 | 34 | /** 35 | * Provide symbol information for component and its contents 36 | * @param documentStateContext The document context for which to provide symbols. 37 | */ 38 | private static getComponentSymbols(documentStateContext: DocumentStateContext): DocumentSymbol[] { 39 | const document: TextDocument = documentStateContext.document; 40 | const component: Component = getComponent(document.uri); 41 | 42 | if (!component) { 43 | return []; 44 | } 45 | 46 | let componentSymbol: DocumentSymbol = new DocumentSymbol( 47 | component.name, 48 | "", 49 | component.isInterface ? SymbolKind.Interface : SymbolKind.Class, 50 | new Range(new Position(0, 0), document.positionAt(document.getText().length)), 51 | component.declarationRange 52 | ); 53 | componentSymbol.children = []; 54 | 55 | // Component properties 56 | let propertySymbols: DocumentSymbol[] = []; 57 | component.properties.forEach((property: Property, propertyKey: string) => { 58 | propertySymbols.push(new DocumentSymbol( 59 | property.name, 60 | "", 61 | SymbolKind.Property, 62 | property.propertyRange, 63 | property.nameRange 64 | )); 65 | }); 66 | componentSymbol.children = componentSymbol.children.concat(propertySymbols); 67 | 68 | // Component variables 69 | let variableSymbols: DocumentSymbol[] = []; 70 | component.variables.forEach((variable: Variable) => { 71 | let detail = ""; 72 | if (variable.scope !== Scope.Unknown) { 73 | detail = `${variable.scope}.${variable.identifier}`; 74 | } 75 | variableSymbols.push(new DocumentSymbol( 76 | variable.identifier, 77 | detail, 78 | usesConstantConvention(variable.identifier) || variable.final ? SymbolKind.Constant : SymbolKind.Variable, 79 | variable.declarationLocation.range, 80 | variable.declarationLocation.range 81 | )); 82 | }); 83 | componentSymbol.children = componentSymbol.children.concat(variableSymbols); 84 | 85 | // Component functions 86 | let functionSymbols: DocumentSymbol[] = []; 87 | component.functions.forEach((userFunction: UserFunction, functionKey: string) => { 88 | let currFuncSymbol: DocumentSymbol = new DocumentSymbol( 89 | userFunction.name, 90 | "", 91 | functionKey === "init" ? SymbolKind.Constructor : SymbolKind.Method, 92 | userFunction.location.range, 93 | userFunction.nameRange 94 | ); 95 | currFuncSymbol.children = []; 96 | 97 | if (!userFunction.isImplicit) { 98 | // Component function local variables 99 | let localVarSymbols: DocumentSymbol[] = []; 100 | const localVariables: Variable[] = getLocalVariables(userFunction, documentStateContext, component.isScript); 101 | localVariables.forEach((variable: Variable) => { 102 | let detail = ""; 103 | if (variable.scope !== Scope.Unknown) { 104 | detail = `${variable.scope}.${variable.identifier}`; 105 | } 106 | localVarSymbols.push(new DocumentSymbol( 107 | variable.identifier, 108 | detail, 109 | usesConstantConvention(variable.identifier) || variable.final ? SymbolKind.Constant : SymbolKind.Variable, 110 | variable.declarationLocation.range, 111 | variable.declarationLocation.range 112 | )); 113 | }); 114 | currFuncSymbol.children = currFuncSymbol.children.concat(localVarSymbols); 115 | } 116 | 117 | functionSymbols.push(currFuncSymbol); 118 | }); 119 | componentSymbol.children = componentSymbol.children.concat(functionSymbols); 120 | 121 | return [componentSymbol]; 122 | } 123 | 124 | /** 125 | * Provide symbol information for templates 126 | * @param documentStateContext The document context for which to provide symbols. 127 | */ 128 | private static getTemplateSymbols(documentStateContext: DocumentStateContext): DocumentSymbol[] { 129 | let templateSymbols: DocumentSymbol[] = []; 130 | // TODO: Cache template variables? 131 | const allVariables: Variable[] = parseVariableAssignments(documentStateContext, false); 132 | allVariables.forEach((variable: Variable) => { 133 | const kind: SymbolKind = usesConstantConvention(variable.identifier) || variable.final ? SymbolKind.Constant : SymbolKind.Variable; 134 | let detail = ""; 135 | if (variable.scope !== Scope.Unknown) { 136 | detail = `${variable.scope}.${variable.identifier}`; 137 | } 138 | templateSymbols.push(new DocumentSymbol( 139 | variable.identifier, 140 | detail, 141 | kind, 142 | variable.declarationLocation.range, 143 | variable.declarationLocation.range 144 | )); 145 | }); 146 | 147 | // TODO: Include inline functions 148 | 149 | return templateSymbols; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/features/docBlocker/docCompletionProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TextDocument, Position, CancellationToken, CompletionItem, CompletionItemProvider, Range, CompletionItemKind 3 | } from "vscode"; 4 | import Documenter from "./documenter"; 5 | import { Component } from "../../entities/component"; 6 | import { getComponent, getGlobalTag } from "../cachedEntities"; 7 | import { Property, Properties } from "../../entities/property"; 8 | import { UserFunction, UserFunctionSignature, Argument, ComponentFunctions } from "../../entities/userFunction"; 9 | import { MySet, MyMap } from "../../utils/collections"; 10 | import { GlobalTag } from "../../entities/globals"; 11 | import { Signature } from "../../entities/signature"; 12 | import { Parameter } from "../../entities/parameter"; 13 | 14 | /** 15 | * Completions provider that can be registered to the language 16 | */ 17 | export default class DocBlockCompletions implements CompletionItemProvider { 18 | 19 | /** 20 | * Implemented function to find and return completions either from 21 | * the tag list or initiate a complex completion 22 | * 23 | * @param document 24 | * @param position 25 | * @param token 26 | */ 27 | public async provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken): Promise { 28 | let result: CompletionItem[] = []; 29 | let wordMatchRange: Range; 30 | 31 | if ((wordMatchRange = document.getWordRangeAtPosition(position, /\/\*\*/)) !== undefined) { 32 | let documenter: Documenter = new Documenter(wordMatchRange.end, document); 33 | 34 | let block = new CompletionItem("/** */", CompletionItemKind.Snippet); 35 | block.range = wordMatchRange; 36 | block.insertText = documenter.autoDocument(); 37 | block.documentation = "Docblock completion"; 38 | result.push(block); 39 | 40 | return result; 41 | } 42 | 43 | const comp: Component = getComponent(document.uri); 44 | if (!comp) { 45 | return result; 46 | } 47 | 48 | if ((wordMatchRange = document.getWordRangeAtPosition(position, /\@[\w$]*(\.[a-z]*)?/)) === undefined) { 49 | return result; 50 | } 51 | 52 | // const tagKeyPattern = / \* @$/; 53 | // const tagSubKeyPattern = / \* @[\w$]+\.$/; 54 | 55 | let tagSuggestions: MyMap = new MyMap(); 56 | let subKeySuggestions: MyMap = new MyMap(); 57 | 58 | let wordRange: Range = document.getWordRangeAtPosition(position); 59 | if (!wordRange) { 60 | wordRange = new Range(position, position); 61 | } 62 | const search: string = document.getText(wordRange); 63 | const lineText: string = document.lineAt(position).text; 64 | const wordPrefix: string = lineText.slice(0, wordRange.start.character); 65 | const prefixChr: string = wordRange.start.character !== 0 ? wordPrefix.substr(wordPrefix.length-1, 1) : ""; 66 | 67 | if (prefixChr !== "@" && prefixChr !== ".") { 68 | return result; 69 | } 70 | 71 | // TODO: Prevent redundant suggestions. 72 | let argumentNames: MySet = new MySet(); 73 | const foundProperty: Properties = comp.properties.filter((prop: Property) => { 74 | return prop.propertyRange.contains(position); 75 | }); 76 | if (foundProperty.size === 1) { 77 | const propertyTag: GlobalTag = getGlobalTag("cfproperty"); 78 | propertyTag.signatures.forEach((sig: Signature) => { 79 | sig.parameters.filter((param: Parameter) => { 80 | return param.name !== "name"; 81 | }).forEach((param: Parameter) => { 82 | tagSuggestions.set(param.name, param.description); 83 | }); 84 | }); 85 | } else { 86 | const foundFunction: ComponentFunctions = comp.functions.filter((func: UserFunction) => { 87 | return func.location.range.contains(position); 88 | }); 89 | if (foundFunction.size === 1) { 90 | const functionTag: GlobalTag = getGlobalTag("cffunction"); 91 | functionTag.signatures.forEach((sig: Signature) => { 92 | sig.parameters.filter((param: Parameter) => { 93 | return param.name !== "name"; 94 | }).forEach((param: Parameter) => { 95 | tagSuggestions.set(param.name, param.description); 96 | }); 97 | }); 98 | 99 | foundFunction.forEach((func: UserFunction) => { 100 | func.signatures.forEach((sig: UserFunctionSignature) => { 101 | sig.parameters.forEach((arg: Argument) => { 102 | argumentNames.add(arg.name); 103 | tagSuggestions.set(arg.name, arg.description); 104 | }); 105 | }); 106 | }); 107 | 108 | const argumentTag: GlobalTag = getGlobalTag("cfargument"); 109 | argumentTag.signatures.forEach((sig: Signature) => { 110 | sig.parameters.filter((param: Parameter) => { 111 | return param.name !== "name"; 112 | }).forEach((param: Parameter) => { 113 | subKeySuggestions.set(param.name, param.description); 114 | }); 115 | }); 116 | } else { 117 | if (comp.isInterface) { 118 | const interfaceTag: GlobalTag = getGlobalTag("cfinterface"); 119 | interfaceTag.signatures.forEach((sig: Signature) => { 120 | sig.parameters.filter((param: Parameter) => { 121 | return param.name !== "name"; 122 | }).forEach((param: Parameter) => { 123 | tagSuggestions.set(param.name, param.description); 124 | }); 125 | }); 126 | } else { 127 | const componentTag: GlobalTag = getGlobalTag("cfcomponent"); 128 | componentTag.signatures.forEach((sig: Signature) => { 129 | sig.parameters.filter((param: Parameter) => { 130 | return param.name !== "name"; 131 | }).forEach((param: Parameter) => { 132 | tagSuggestions.set(param.name, param.description); 133 | }); 134 | }); 135 | } 136 | } 137 | } 138 | 139 | let suggestions: MyMap; 140 | if (prefixChr === "." && argumentNames.size !== 0) { 141 | let prevWordRange: Range = document.getWordRangeAtPosition(wordRange.start.translate(0, -1)); 142 | if (!prevWordRange) { 143 | prevWordRange = new Range(position, position); 144 | } 145 | const prevWord: string = document.getText(prevWordRange); 146 | if (argumentNames.has(prevWord)) { 147 | suggestions = subKeySuggestions; 148 | } 149 | } else if (prefixChr === "@") { 150 | suggestions = tagSuggestions; 151 | } 152 | 153 | if (suggestions) { 154 | suggestions.filter((_suggestDesc: string, suggestionName: string) => { 155 | return suggestionName.match(search) !== null; 156 | }).forEach((suggestDesc: string, suggestionName: string) => { 157 | let item = new CompletionItem(suggestionName, CompletionItemKind.Property); 158 | item.documentation = suggestDesc; 159 | result.push(item); 160 | }); 161 | } 162 | 163 | return result; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/entities/globals.ts: -------------------------------------------------------------------------------- 1 | import { SnippetString } from "vscode"; 2 | import { MyMap, NameWithOptionalValue } from "../utils/collections"; 3 | import { ATTRIBUTES_PATTERN, IncludeAttributesSetType, AttributeQuoteType } from "./attribute"; 4 | import { DataType } from "./dataType"; 5 | import { Function } from "./function"; 6 | import { Parameter } from "./parameter"; 7 | import { Signature } from "./signature"; 8 | import { getCfStartTagPattern } from "./tag"; 9 | import { equalsIgnoreCase, getQuote } from "../utils/textUtil"; 10 | 11 | export interface GlobalEntity { 12 | name: string; 13 | syntax: string; 14 | description: string; 15 | signatures: Signature[]; 16 | } 17 | export interface GlobalFunction extends GlobalEntity, Function { } 18 | export interface GlobalFunctions { 19 | [name: string]: GlobalFunction; 20 | } 21 | export interface MemberFunction extends Function { 22 | name: string; 23 | syntax: string; 24 | description: string; 25 | returntype: DataType; 26 | signatures: Signature[]; 27 | } 28 | export interface MemberFunctionsByType extends MyMap> { } 29 | export interface GlobalTag extends GlobalEntity { 30 | scriptSyntax?: string; 31 | hasBody: boolean; 32 | } 33 | export interface GlobalTags { 34 | [name: string]: GlobalTag; 35 | } 36 | 37 | /** 38 | * TODO: Implement 39 | * Returns the data type of the member function variant of the given global function 40 | * @param functionName The global function name 41 | */ 42 | // export function getMemberFunctionType(functionName: string): DataType { 43 | // return DataType.Any; 44 | // } 45 | 46 | /** 47 | * Transforms the global tag syntax into script syntax 48 | * @param globalTag The global tag for which the syntax will be transformed 49 | */ 50 | export function globalTagSyntaxToScript(globalTag: GlobalTag): string { 51 | let attributes: string[] = []; 52 | const cfStartTagPattern = getCfStartTagPattern(); 53 | const attributeStr: string = cfStartTagPattern.exec(globalTag.syntax)[3]; 54 | if (attributeStr) { 55 | let attributeMatch: RegExpExecArray = null; 56 | while (attributeMatch = ATTRIBUTES_PATTERN.exec(attributeStr)) { 57 | attributes.push(attributeMatch[0]); 58 | } 59 | } 60 | 61 | return `${globalTag.name}(${attributes.join(", ")})`; 62 | } 63 | 64 | // TODO: Check cfml.suggest.globalTags.attributes.quoteType 65 | /** 66 | * Constructs a snippet for the given global tag which includes attributes 67 | * @param globalTag The global tag for which to construct the snippet 68 | * @param includeAttributesSetType Indicates which set of attributes to include in the snippet 69 | * @param attributeQuoteType The type of quote to use for attributes 70 | * @param includeAttributesCustom Provides an optional set of attributes which overrides the set type 71 | * @param includeDefaultValue Whether to fill the attribute value with the default if it exists 72 | * @param isScript Whether this snippet for a script tag 73 | */ 74 | export function constructTagSnippet( 75 | globalTag: GlobalTag, 76 | includeAttributesSetType: IncludeAttributesSetType = IncludeAttributesSetType.Required, 77 | attributeQuoteType: AttributeQuoteType = AttributeQuoteType.Double, 78 | includeAttributesCustom?: NameWithOptionalValue[], 79 | includeDefaultValue: boolean = false, 80 | isScript: boolean = false 81 | ): SnippetString | undefined { 82 | let tagSnippet: SnippetString; 83 | 84 | if (includeAttributesSetType !== IncludeAttributesSetType.None || (includeAttributesCustom !== undefined && includeAttributesCustom.length > 0)) { 85 | let snippetParamParts: string[] = []; 86 | if (globalTag.signatures.length > 0) { 87 | const sig: Signature = globalTag.signatures[0]; 88 | 89 | let parameters: Parameter[] = sig.parameters; 90 | if (includeAttributesCustom !== undefined) { 91 | parameters = includeAttributesCustom.map((attributeEntry: NameWithOptionalValue) => { 92 | return sig.parameters.find((param: Parameter) => { 93 | return equalsIgnoreCase(param.name, attributeEntry.name); 94 | }); 95 | }).filter((param: Parameter) => { 96 | return param !== undefined; 97 | }); 98 | } else if (includeAttributesSetType === IncludeAttributesSetType.Required) { 99 | parameters = parameters.filter((param: Parameter) => { 100 | return param.required; 101 | }); 102 | } 103 | snippetParamParts = parameters.map((param: Parameter, index: number) => { 104 | return constructAttributeSnippet(param, index, attributeQuoteType, includeDefaultValue, includeAttributesCustom); 105 | }); 106 | } 107 | 108 | let snippetString: string = ""; 109 | 110 | if (isScript) { 111 | snippetString = `${globalTag.name}(${snippetParamParts.join(", ")})$0`; 112 | } else { 113 | if (snippetParamParts.length > 0) { 114 | snippetString = `${globalTag.name} ${snippetParamParts.join(" ")}$0`; 115 | } else { 116 | snippetString = globalTag.name; 117 | } 118 | } 119 | 120 | tagSnippet = new SnippetString(snippetString); 121 | } 122 | 123 | return tagSnippet; 124 | } 125 | 126 | /** 127 | * Constructs a snippet for the given attribute 128 | * @param param 129 | * @param index 130 | * @param attributeQuoteType The type of quote to use for attributes 131 | * @param includeDefaultValue Whether to fill the attribute value with the default if it exists 132 | * @param includeAttributesCustom Provides an optional set of attributes which overrides the set type 133 | */ 134 | export function constructAttributeSnippet( 135 | param: Parameter, 136 | index: number, 137 | attributeQuoteType: AttributeQuoteType = AttributeQuoteType.Double, 138 | includeDefaultValue: boolean = false, 139 | includeAttributesCustom?: NameWithOptionalValue[] 140 | ): string { 141 | const tabstopNumber: number = index + 1; 142 | 143 | /* 144 | if (param.enumeratedValues && param.enumeratedValues.length > 0 && !param.enumeratedValues.includes("|") && !param.enumeratedValues.includes(",")) { 145 | snippetString += `\${${tabstopNumber}|${param.enumeratedValues.join(",")}|}`; 146 | } else if (param.dataType === DataType.Boolean) { 147 | snippetString += `\${${tabstopNumber}|true,false|}`; 148 | } else { 149 | snippetString += "$" + tabstopNumber; 150 | } 151 | */ 152 | 153 | let placeholder: string = ""; 154 | 155 | let customValue: string; 156 | if (includeAttributesCustom !== undefined) { 157 | const customEntry: NameWithOptionalValue = includeAttributesCustom.find((attributeEntry: NameWithOptionalValue) => { 158 | return equalsIgnoreCase(attributeEntry.name, param.name); 159 | }); 160 | 161 | if (customEntry !== undefined) { 162 | customValue = customEntry.value; 163 | } 164 | } 165 | 166 | if (customValue !== undefined) { 167 | placeholder = customValue; 168 | } else if (includeDefaultValue && param.default) { 169 | placeholder = param.default; 170 | } 171 | 172 | const quoteStr: string = getQuote(attributeQuoteType); 173 | 174 | return `${param.name}=${quoteStr}\${${tabstopNumber}:${placeholder}}${quoteStr}`; 175 | } 176 | -------------------------------------------------------------------------------- /src/entities/catch.ts: -------------------------------------------------------------------------------- 1 | import { CompletionEntry } from "../features/completionItemProvider"; 2 | import { DocumentStateContext } from "../utils/documentUtil"; 3 | import { TextDocument, Range } from "vscode"; 4 | import { getClosingPosition, getCfScriptRanges } from "../utils/contextUtil"; 5 | import { parseTags, Tag } from "./tag"; 6 | 7 | export interface CatchPropertyDetails extends CompletionEntry { 8 | appliesToTypes?: string[]; 9 | } 10 | 11 | export interface CatchProperties { 12 | [propertyName: string]: CatchPropertyDetails; 13 | } 14 | 15 | export const catchProperties: CatchProperties = { 16 | "type": { 17 | detail: "(property) Exception.type", 18 | description: "Type: Exception type." 19 | }, 20 | "message": { 21 | detail: "(property) Exception.message", 22 | description: "Message: Exception’s diagnostic message, if provided; otherwise, an empty string." 23 | }, 24 | "detail": { 25 | detail: "(property) Exception.detail", 26 | description: "Detailed message from the CFML interpreter or specified in a cfthrow tag. When the exception is generated by ColdFusion (and not cfthrow), the message can contain HTML formatting and can help determine which tag threw the exception." 27 | }, 28 | "tagContext": { 29 | detail: "(property) Exception.tagContext", 30 | description: "An array of tag context structures, each representing one level of the active tag context at the time of the exception." 31 | }, 32 | "nativeErrorCode": { 33 | detail: "(property) Exception.nativeErrorCode", 34 | description: "Applies to type=\"database\". Native error code associated with exception. Database drivers typically provide error codes to diagnose failing database operations. Default value is -1.", 35 | appliesToTypes: ["database"] 36 | }, 37 | "sqlState": { 38 | detail: "(property) Exception.sqlState", 39 | description: "Applies to type=\"database\". SQLState associated with exception. Database drivers typically provide error codes to help diagnose failing database operations. Default value is -1.", 40 | appliesToTypes: ["database"] 41 | }, 42 | "sql": { 43 | detail: "(property) Exception.sql", 44 | description: "Applies to type=\"database\". The SQL statement sent to the data source.", 45 | appliesToTypes: ["database"] 46 | }, 47 | "queryError": { 48 | detail: "(property) Exception.queryError", 49 | description: "Applies to type=\"database\". The error message as reported by the database driver.", 50 | appliesToTypes: ["database"] 51 | }, 52 | "where": { 53 | detail: "(property) Exception.where", 54 | description: "Applies to type=\"database\". If the query uses the cfqueryparam tag, query parameter name-value pairs.", 55 | appliesToTypes: ["database"] 56 | }, 57 | "errNumber": { 58 | detail: "(property) Exception.errNumber", 59 | description: "Applies to type=\"expression\". Internal expression error number.", 60 | appliesToTypes: ["expression"] 61 | }, 62 | "missingFileName": { 63 | detail: "(property) Exception.missingFileName", 64 | description: "Applies to type=\"missingInclude\". Name of file that could not be included.", 65 | appliesToTypes: ["missinginclude"] 66 | }, 67 | "lockName": { 68 | detail: "(property) Exception.lockName", 69 | description: "Applies to type=\"lock\". Name of affected lock (if the lock is unnamed, the value is \"anonymous\").", 70 | appliesToTypes: ["lock"] 71 | }, 72 | "lockOperation": { 73 | detail: "(property) Exception.lockOperation", 74 | description: "Applies to type=\"lock\". Operation that failed (Timeout, Create Mutex, or Unknown).", 75 | appliesToTypes: ["lock"] 76 | }, 77 | "errorCode": { 78 | detail: "(property) Exception.errorCode", 79 | description: "Applies to type=\"custom\". String error code." 80 | }, 81 | "extendedInfo": { 82 | detail: "(property) Exception.extendedInfo", 83 | description: "Applies to type=\"application\" and \"custom\". Custom error message; information that the default exception handler does not display.", 84 | appliesToTypes: ["application"] 85 | }, 86 | }; 87 | 88 | // Type is optional in Lucee 89 | export const scriptCatchPattern: RegExp = /\}\s*catch\s*\(\s*([A-Za-z0-9_\.$]+)\s+([_$a-zA-Z][$\w]*)\s*\)\s*\{/gi; 90 | 91 | export interface CatchInfo { 92 | type: string; 93 | variableName: string; 94 | bodyRange: Range; 95 | } 96 | 97 | /** 98 | * Parses the catches in the document and returns an array of catch information 99 | * @param documentStateContext The context information for a TextDocument in which to parse the CFScript functions 100 | * @param isScript Whether this document or range is defined entirely in CFScript 101 | * @param docRange Range within which to check 102 | */ 103 | export function parseCatches(documentStateContext: DocumentStateContext, isScript: boolean, docRange?: Range): CatchInfo[] { 104 | let catchInfoArr: CatchInfo[] = []; 105 | const document: TextDocument = documentStateContext.document; 106 | let textOffset: number = 0; 107 | let documentText: string = documentStateContext.sanitizedDocumentText; 108 | 109 | if (docRange && document.validateRange(docRange)) { 110 | textOffset = document.offsetAt(docRange.start); 111 | documentText = documentText.slice(textOffset, document.offsetAt(docRange.end)); 112 | } 113 | 114 | if (isScript) { 115 | let scriptCatchMatch: RegExpExecArray = null; 116 | while (scriptCatchMatch = scriptCatchPattern.exec(documentText)) { 117 | const catchType = scriptCatchMatch[1] ? scriptCatchMatch[1] : "any"; 118 | const catchVariable = scriptCatchMatch[2]; 119 | 120 | const catchBodyStartOffset = textOffset + scriptCatchMatch.index + scriptCatchMatch[0].length; 121 | const catchBodyEndPosition = getClosingPosition(documentStateContext, catchBodyStartOffset, "}"); 122 | 123 | const catchBodyRange: Range = new Range( 124 | document.positionAt(catchBodyStartOffset), 125 | catchBodyEndPosition.translate(0, -1) 126 | ); 127 | 128 | let catchInfo: CatchInfo = { 129 | type: catchType, 130 | variableName: catchVariable, 131 | bodyRange: catchBodyRange 132 | }; 133 | 134 | catchInfoArr.push(catchInfo); 135 | } 136 | } else { 137 | const tagName: string = "cfcatch"; 138 | const tags: Tag[] = parseTags(documentStateContext, tagName, docRange); 139 | 140 | tags.forEach((tag: Tag) => { 141 | if (tag.bodyRange === undefined) { 142 | return; 143 | } 144 | 145 | let catchType: string = "any"; 146 | let catchVariable: string = tagName; 147 | 148 | if (tag.attributes.has("type")) { 149 | catchType = tag.attributes.get("type").value; 150 | } 151 | 152 | if (tag.attributes.has("name")) { 153 | catchVariable = tag.attributes.get("name").value; 154 | } 155 | 156 | let catchInfo: CatchInfo = { 157 | type: catchType, 158 | variableName: catchVariable, 159 | bodyRange: tag.bodyRange 160 | }; 161 | 162 | catchInfoArr.push(catchInfo); 163 | }); 164 | 165 | // Check cfscript sections 166 | const cfScriptRanges: Range[] = getCfScriptRanges(document, docRange); 167 | cfScriptRanges.forEach((range: Range) => { 168 | const cfscriptCatches: CatchInfo[] = parseCatches(documentStateContext, true, range); 169 | 170 | catchInfoArr = catchInfoArr.concat(cfscriptCatches); 171 | }); 172 | } 173 | 174 | return catchInfoArr; 175 | } 176 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the CFML extension will be documented in this file. 4 | 5 | ## [0.5.4] - 2022-01-05 6 | 7 | - Improved grammar 8 | - Improved command registration and availability 9 | - Now respects `files.exclude` for features 10 | - Removed usage of CommandBox server schema. Please use [`ortus-solutions.vscode-commandbox`](https://github.com/Ortus-Solutions/vscode-commandbox) instead. 11 | - Improved code documentation 12 | 13 | ## [0.5.3] - 2019-02-07 14 | 15 | - Improved component parsing 16 | - Added some more existence checks 17 | - Fixed a hover error for expression tags 18 | - Fixed a color provider error 19 | - Fixed a couple issues with signature help detection 20 | - Fixed a couple grammar scopes ([\#29](https://github.com/KamasamaK/vscode-cfml/issues/29)) 21 | - Fixed issue when reading compiled files 22 | - Integrated `vscode-css-languageservice` and `vscode-html-languageservice` instead of using copied data 23 | 24 | ## [0.5.2] - 2019-01-18 25 | 26 | - Added some existence checks 27 | - Added some exception handling 28 | 29 | ## [0.5.1] - 2019-01-17 30 | 31 | - Improved support and fixed bugs for interfaces and abstract functions ([\#27](https://github.com/KamasamaK/vscode-cfml/issues/27)) 32 | - Fixed a minor issue with signature help detection in a specific case 33 | 34 | ## [0.5.0] - 2019-01-13 35 | 36 | - Update minimum version of VS Code to v1.30 37 | - Update `target` and `lib` in tsconfig 38 | - Added `DefinitionLink` support for providing definitions. This allows a full component path to be used for definition links. 39 | - Added doc links for each engine on hover ([\#14](https://github.com/KamasamaK/vscode-cfml/issues/14)) 40 | - Added completions for `this`-scoped variables for external references of the component ([\#26](https://github.com/KamasamaK/vscode-cfml/pull/26)) 41 | - Added command `cfml.foldAllFunctions` 42 | - Added setting for completing tag attributes with quotes -- `cfml.suggest.globalTags.attributes.quoteType` ([\#24](https://github.com/KamasamaK/vscode-cfml/issues/24)) 43 | - Added new `onEnterRules` rule for when the cursor is between an opening and closing tag ([\#23](https://github.com/KamasamaK/vscode-cfml/issues/23) and [\#24](https://github.com/KamasamaK/vscode-cfml/issues/24)) 44 | - Added setting for preferred case in global function suggestions -- `cfml.suggest.globalFunctions.firstLetterCase` ([\#25](https://github.com/KamasamaK/vscode-cfml/issues/25)) 45 | - Added folding region markers to language configuration 46 | - Added hover and completion for HTML tags 47 | - Added hover and completion for CSS properties 48 | - Added color support for CSS property values 49 | - Changed `ParameterInformation.label` to use new tuple type 50 | - Removed Emmet setting and added instructions in `README` 51 | - Fixed document symbols for implicit functions 52 | - Fixed issue displaying multiple signatures 53 | - Added CommandBox `server.json` schema 54 | - Added progress notification when caching all components 55 | - Improved parsing for signature help and added check for named parameters 56 | 57 | ## [0.4.1] - 2018-08-09 58 | 59 | - Update minimum version of VS Code to v1.25 60 | - Added commands `cfml.openCfDocs` and `cfml.openEngineDocs` ([\#14](https://github.com/KamasamaK/vscode-cfml/issues/14)) 61 | - Added notification for auto-close-tag extension when not installed and setting is enabled 62 | - Added support for new ACF 2018 syntax 63 | - Added a setting that will enable a definition search in a workspace if a reliable function definition cannot be found 64 | - Improved support for functions defined in cfm files 65 | - Improved suggestions for closures assigned to variables 66 | - Fixed exception suggestions for type `any` 67 | - Fixed syntax highlighting issue for variable properties with numeric keys 68 | - Updated Tasks to 2.0.0 69 | - Updated `DocumentSymbolProvider` to provide new `DocumentSymbol` type 70 | 71 | ## [0.4.0] - 2018-06-04 72 | 73 | - Update minimum version of VS Code to v1.22 74 | - Added support for custom mappings 75 | - Added setting for whether to provide definitions 76 | - Added more type definitions 77 | - Added scopes to settings to indicate whether they are resource-based or window-based 78 | - Added ability and configuration to have attributes populated for global tag completions 79 | - Added command to open Application file for active document 80 | - Added command to go to matching CFML tag 81 | - Application and Server variables initialized in their respective components are now cached and properly considered for various features 82 | - Improved catch information and suggestions 83 | - Improved suggestions for queries initialized in the same file/block 84 | - Improved docblock parsing 85 | - Fixed detection of certain variable assignments within switch statements 86 | - Fixed some syntax highlighting issues ([\#12](https://github.com/KamasamaK/vscode-cfml/issues/12)+) 87 | - Limited suggestions for script tags to only be in script context 88 | - Some refactoring 89 | 90 | ## [0.3.1] - 2018-02-12 91 | 92 | - Added syntax highlighting for HTML style attribute 93 | - Added hover for external component functions 94 | - Added signature help for implicit getters/setters 95 | - Added signature help for external component functions 96 | - Added definitions for external component functions 97 | - Added definitions for variables within templates 98 | 99 | ## [0.3.0] - 2018-01-22 100 | 101 | - Added more ways to check context 102 | - Added completions for external component functions 103 | - Added completions for query properties 104 | - Added completions for component dot-paths 105 | - Added completions for enumerated values for global tag attributes 106 | - Added completions for script global tags 107 | - Added definition for arguments 108 | - Added definition for local variables 109 | - Added definition for inherited functions 110 | - Added definition for application variables 111 | - Added type definitions within components 112 | - Added hover for global tag attributes 113 | - Added hover for inherited functions 114 | - Added signature help for inherited functions 115 | - Added signature help for constructor when using `new` syntax 116 | - Added variable parsing for for-in statements 117 | - Added option `noImplicitReturns` to tsconfig 118 | - Made some additional functions `async` 119 | - Fixed some case sensitivity issues in CFML grammar/syntax 120 | - Updated embedded syntaxes for HTML, CSS, JavaScript, and SQL 121 | 122 | ## [0.2.0] - 2017-11-29 123 | 124 | - Update minimum version of VS Code to v1.18 125 | - Added global definition filtering based on engine 126 | - Improved type inference 127 | - Changed signature format 128 | - Argument type now indicates component name 129 | - Improved syntax highlighting for properties 130 | - Now able to ignore CFML comments 131 | - Added variables assigned from tag attributes 132 | - Added option `noUnusedLocals` to tsconfig 133 | 134 | ## [0.1.4] - 2017-11-13 135 | 136 | - Added `cfcatch` help 137 | - Improved attribute parsing 138 | - Added param parsing 139 | - Using new `MarkdownString` type where applicable 140 | - Added hash (`#`) to `autoClosingPairs` and set syntax to have contents of hash pairs as embedded code where applicable 141 | 142 | ## [0.1.3] - 2017-10-05 143 | 144 | - Added docblock completion 145 | - Improved tag attribute name completion 146 | - Minor syntax additions 147 | 148 | ## [0.1.2] - 2017-10-02 149 | 150 | - Corrected checks for existence of certain other extensions 151 | 152 | ## [0.1.1] - 2017-10-02 153 | 154 | - Corrected issue with CFLint running for all indexed files 155 | - Fixed issue causing publication to fail 156 | 157 | ## [0.1.0] - 2017-10-01 158 | 159 | - Initial release 160 | -------------------------------------------------------------------------------- /src/entities/dataType.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | import { equalsIgnoreCase } from "../utils/textUtil"; 3 | import { componentPathToUri } from "./component"; 4 | import { queryValuePattern } from "./query"; 5 | import { functionValuePattern } from "./userFunction"; 6 | 7 | export enum DataType { 8 | Any = "any", 9 | Array = "array", 10 | Binary = "binary", 11 | Boolean = "boolean", 12 | Component = "component", 13 | Date = "date", 14 | Function = "function", 15 | GUID = "guid", 16 | Numeric = "numeric", 17 | Query = "query", 18 | String = "string", 19 | Struct = "struct", 20 | UUID = "uuid", 21 | VariableName = "variablename", 22 | Void = "void", 23 | XML = "xml" 24 | } 25 | 26 | export namespace DataType { 27 | /** 28 | * Resolves a string value of data type to an enumeration member 29 | * @param dataType The data type string to resolve 30 | */ 31 | export function valueOf(dataType: string): DataType { 32 | switch (dataType.toLowerCase()) { 33 | case "any": 34 | return DataType.Any; 35 | case "array": 36 | return DataType.Array; 37 | case "binary": 38 | return DataType.Binary; 39 | case "boolean": 40 | return DataType.Boolean; 41 | case "component": 42 | return DataType.Component; 43 | case "date": 44 | return DataType.Date; 45 | case "function": 46 | return DataType.Function; 47 | case "guid": 48 | return DataType.GUID; 49 | case "numeric": 50 | return DataType.Numeric; 51 | case "query": 52 | return DataType.Query; 53 | case "string": 54 | return DataType.String; 55 | case "struct": 56 | return DataType.Struct; 57 | case "uuid": 58 | return DataType.UUID; 59 | case "variablename": 60 | return DataType.VariableName; 61 | case "void": 62 | return DataType.Void; 63 | case "xml": 64 | return DataType.XML; 65 | default: 66 | return DataType.Any; 67 | } 68 | } 69 | 70 | /** 71 | * Resolves a string value of param type to an enumeration member 72 | * @param paramType The param type string to resolve 73 | */ 74 | export function paramTypeToDataType(paramType: string): DataType { 75 | switch (paramType.toLowerCase()) { 76 | case "any": 77 | return DataType.Any; 78 | case "array": 79 | return DataType.Array; 80 | case "binary": 81 | return DataType.Binary; 82 | case "boolean": 83 | return DataType.Boolean; 84 | /* 85 | case "component": 86 | return DataType.Component; 87 | */ 88 | case "date": case "eurodate": case "usdate": 89 | return DataType.Date; 90 | case "function": 91 | return DataType.Function; 92 | case "guid": 93 | return DataType.GUID; 94 | case "numeric": case "float": case "integer": case "range": 95 | return DataType.Numeric; 96 | case "query": 97 | return DataType.Query; 98 | case "string": case "creditcard": case "email": case "regex": case "regular_expression": 99 | case "ssn": case "social_security_number": case "telephone": case "url": case "zipcode": 100 | return DataType.String; 101 | case "struct": 102 | return DataType.Struct; 103 | case "uuid": 104 | return DataType.UUID; 105 | case "variablename": 106 | return DataType.VariableName; 107 | case "xml": 108 | return DataType.XML; 109 | default: 110 | return DataType.Any; 111 | } 112 | } 113 | 114 | /** 115 | * Validates whether a string is numeric 116 | * @param numStr A string to check 117 | */ 118 | export function isNumeric(numStr: string): boolean { 119 | let numStrTest: string = numStr; 120 | if (/^(["'])[0-9.]+\1$/.test(numStrTest)) { 121 | numStrTest = numStrTest.slice(1, -1); 122 | } 123 | return (!isNaN(parseFloat(numStrTest)) && isFinite(parseFloat(numStrTest))); 124 | } 125 | 126 | /** 127 | * Validates whether a string is a string literal 128 | * @param str A string to check 129 | */ 130 | export function isStringLiteral(str: string): boolean { 131 | const trimmedStr: string = str.trim(); 132 | 133 | return (trimmedStr.length > 1 && ((trimmedStr.startsWith("'") && trimmedStr.endsWith("'")) || (trimmedStr.startsWith('"') && trimmedStr.endsWith('"')))); 134 | } 135 | 136 | /** 137 | * Gets the string literal value from the given CFML string literal 138 | * @param str A string literal from which to get the string value 139 | */ 140 | export function getStringLiteralValue(str: string): string { 141 | let trimmedStr: string = str.trim(); 142 | const stringDelimiter: string = trimmedStr.charAt(0); 143 | trimmedStr = trimmedStr.slice(1, -1); 144 | let stringValue: string = ""; 145 | 146 | let previousChar: string = ""; 147 | let currentChar: string = ""; 148 | for (let idx = 0; idx < trimmedStr.length; idx++) { 149 | currentChar = trimmedStr.charAt(idx); 150 | 151 | // Skip if escaped 152 | if (previousChar === stringDelimiter && currentChar === stringDelimiter) { 153 | previousChar = ""; 154 | continue; 155 | } 156 | 157 | stringValue += currentChar; 158 | 159 | previousChar = currentChar; 160 | } 161 | 162 | return stringValue; 163 | } 164 | 165 | /** 166 | * Checks whether a string is a valid data type 167 | * @param dataType A string to check 168 | */ 169 | function isDataType(dataType: string): boolean { 170 | return (dataType && (equalsIgnoreCase(dataType, "any") || valueOf(dataType) !== DataType.Any)); 171 | } 172 | 173 | /** 174 | * Returns the truthy value of a string 175 | * @param boolStr A string to evaluate 176 | */ 177 | export function isTruthy(boolStr: string): boolean { 178 | if (equalsIgnoreCase(boolStr, "true") || equalsIgnoreCase(boolStr, "yes")) { 179 | return true; 180 | } 181 | if (isNumeric(boolStr)) { 182 | return (parseFloat(boolStr) !== 0); 183 | } 184 | 185 | return false; 186 | } 187 | 188 | /** 189 | * Gets the data type and if applicable component URI from given string. 190 | * @param dataType The string to check 191 | * @param documentUri The document's URI that contains this type string 192 | */ 193 | export function getDataTypeAndUri(dataType: string, documentUri: Uri): [DataType, Uri] { 194 | if (!dataType) { 195 | return undefined; 196 | } 197 | 198 | if (isDataType(dataType)) { 199 | return [valueOf(dataType), null]; 200 | } else { 201 | const typeUri: Uri = componentPathToUri(dataType, documentUri); 202 | if (typeUri) { 203 | return [DataType.Component, typeUri]; 204 | } 205 | } 206 | 207 | return undefined; 208 | } 209 | 210 | /** 211 | * Analyzes the given value to try to infer its type 212 | * @param value The value to analyze 213 | * @param documentUri The URI of the document containing the value 214 | */ 215 | export function inferDataTypeFromValue(value: string, documentUri: Uri): [DataType, Uri] { 216 | if (value.length === 0) { 217 | return [DataType.String, null]; 218 | } 219 | 220 | if (/^(['"])?(false|true|no|yes)\1$/i.test(value)) { 221 | return [DataType.Boolean, null]; 222 | } 223 | 224 | if (isNumeric(value)) { 225 | return [DataType.Numeric, null]; 226 | } 227 | 228 | if (/^(["'])(?!#)/.test(value)) { 229 | return [DataType.String, null]; 230 | } 231 | 232 | if (functionValuePattern.test(value)) { 233 | return [DataType.Function, null]; 234 | } 235 | 236 | if (/^(?:["']\s*#\s*)?(arrayNew\(|\[)/i.test(value)) { 237 | return [DataType.Array, null]; 238 | } 239 | 240 | if (queryValuePattern.test(value)) { 241 | return [DataType.Query, null]; 242 | } 243 | 244 | if (/^(?:["']\s*#\s*)?(structNew\(|\{)/i.test(value)) { 245 | return [DataType.Struct, null]; 246 | } 247 | 248 | if (/^(?:["']\s*#\s*)?(createDate(Time)?\()/i.test(value)) { 249 | return [DataType.Date, null]; 250 | } 251 | 252 | const objectMatch1 = /^(?:["']\s*#\s*)?(createObject\((["'])component\2\s*,\s*(["'])([^'"]+)\3)/i.exec(value); 253 | if (objectMatch1) { 254 | const findUri: [DataType, Uri] = getDataTypeAndUri(objectMatch1[4], documentUri); 255 | if (findUri) { 256 | return findUri; 257 | } 258 | return [DataType.Component, null]; 259 | } 260 | 261 | const objectMatch2 = /^(?:["']\s*#\s*)?(new\s+(["'])?([^'"(]+)\2\()/i.exec(value); 262 | if (objectMatch2) { 263 | const findUri: [DataType, Uri] = getDataTypeAndUri(objectMatch2[3], documentUri); 264 | if (findUri) { 265 | return findUri; 266 | } 267 | return [DataType.Component, null]; 268 | } 269 | 270 | // TODO: Check against functions and use its return type 271 | 272 | return [DataType.Any, null]; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/entities/property.ts: -------------------------------------------------------------------------------- 1 | import { Location, Range, TextDocument, Uri } from "vscode"; 2 | import { MyMap, MySet } from "../utils/collections"; 3 | import { Attribute, Attributes, parseAttributes } from "./attribute"; 4 | import { DataType } from "./dataType"; 5 | import { DocBlockKeyValue, parseDocBlock } from "./docblock"; 6 | import { Access, UserFunction, UserFunctionSignature } from "./userFunction"; 7 | import { DocumentStateContext } from "../utils/documentUtil"; 8 | 9 | const propertyPattern: RegExp = /((\/\*\*((?:\*(?!\/)|[^*])*)\*\/\s+)?(?:]*)/gi; 10 | // const attributePattern: RegExp = /\b(\w+)\b(?:\s*=\s*(?:(['"])(.*?)\2|([a-z0-9:.]+)))?/gi; 11 | 12 | const propertyAttributeNames: MySet = new MySet([ 13 | "name", 14 | "displayname", 15 | "hint", 16 | "default", 17 | "required", 18 | "type", 19 | "serializable", 20 | "getter", 21 | "setter" 22 | ]); 23 | const booleanAttributes: MySet = new MySet([ 24 | "getter", 25 | "setter" 26 | ]); 27 | 28 | export interface Property { 29 | name: string; 30 | dataType: DataType; 31 | dataTypeComponentUri?: Uri; // Only when dataType is Component 32 | description?: string; 33 | getter?: boolean; 34 | setter?: boolean; 35 | nameRange: Range; 36 | dataTypeRange?: Range; 37 | propertyRange: Range; 38 | default?: string; 39 | } 40 | 41 | // Collection of properties for a particular component. Key is property name lowercased. 42 | export class Properties extends MyMap { } 43 | 44 | /** 45 | * Returns an array of Property objects that define properties within the given component 46 | * @param document The document to parse which should represent a component 47 | */ 48 | export function parseProperties(documentStateContext: DocumentStateContext): Properties { 49 | let properties: Properties = new Properties(); 50 | const document: TextDocument = documentStateContext.document; 51 | const componentText: string = document.getText(); 52 | let propertyMatch: RegExpExecArray = null; 53 | while (propertyMatch = propertyPattern.exec(componentText)) { 54 | const propertyAttributePrefix: string = propertyMatch[1]; 55 | const propertyFullDoc: string = propertyMatch[2]; 56 | const propertyDocContent: string = propertyMatch[3]; 57 | const propertyAttrs: string = propertyMatch[4]; 58 | let property: Property = { 59 | name: "", 60 | dataType: DataType.Any, 61 | description: "", 62 | nameRange: new Range( 63 | document.positionAt(propertyMatch.index), 64 | document.positionAt(propertyMatch.index + propertyMatch[0].length) 65 | ), 66 | propertyRange: new Range( 67 | document.positionAt(propertyMatch.index), 68 | document.positionAt(propertyMatch.index + propertyMatch[0].length + 1) 69 | ) 70 | }; 71 | 72 | if (propertyFullDoc) { 73 | const propertyDocBlockParsed: DocBlockKeyValue[] = parseDocBlock(document, 74 | new Range( 75 | document.positionAt(propertyMatch.index + 3), 76 | document.positionAt(propertyMatch.index + 3 + propertyDocContent.length) 77 | ) 78 | ); 79 | 80 | propertyDocBlockParsed.forEach((docElem: DocBlockKeyValue) => { 81 | const activeKey: string = docElem.key; 82 | if (activeKey === "type") { 83 | const checkDataType: [DataType, Uri] = DataType.getDataTypeAndUri(docElem.value, document.uri); 84 | if (checkDataType) { 85 | property.dataType = checkDataType[0]; 86 | if (checkDataType[1]) { 87 | property.dataTypeComponentUri = checkDataType[1]; 88 | } 89 | 90 | property.dataTypeRange = docElem.valueRange; 91 | } 92 | } else if (activeKey === "hint") { 93 | property.description = docElem.value; 94 | } else if (booleanAttributes.has(activeKey)) { 95 | property[activeKey] = DataType.isTruthy(docElem.value); 96 | } else { 97 | property[activeKey] = docElem.value; 98 | } 99 | }); 100 | } 101 | 102 | if (/=/.test(propertyAttrs)) { 103 | const propertyAttributesOffset: number = propertyMatch.index + propertyAttributePrefix.length; 104 | const propertyAttributeRange = new Range( 105 | document.positionAt(propertyAttributesOffset), 106 | document.positionAt(propertyAttributesOffset + propertyAttrs.length) 107 | ); 108 | const parsedPropertyAttributes: Attributes = parseAttributes(document, propertyAttributeRange, propertyAttributeNames); 109 | if (!parsedPropertyAttributes.has("name")) { 110 | continue; 111 | } 112 | 113 | parsedPropertyAttributes.forEach((attr: Attribute, attrKey: string) => { 114 | if (attrKey === "name") { 115 | property.name = attr.value; 116 | property.nameRange = attr.valueRange; 117 | } else if (attrKey === "type") { 118 | const checkDataType: [DataType, Uri] = DataType.getDataTypeAndUri(attr.value, document.uri); 119 | if (checkDataType) { 120 | property.dataType = checkDataType[0]; 121 | if (checkDataType[1]) { 122 | property.dataTypeComponentUri = checkDataType[1]; 123 | } 124 | 125 | property.dataTypeRange = attr.valueRange; 126 | } 127 | } else if (attrKey === "hint") { 128 | property.description = attr.value; 129 | } else if (booleanAttributes.has(attrKey)) { 130 | property[attrKey] = DataType.isTruthy(attr.value); 131 | } else { 132 | property[attrKey] = attr.value; 133 | } 134 | }); 135 | } else { 136 | const parsedPropertyAttributes: RegExpExecArray = /\s*(\S+)\s+([\w$]+)\s*$/.exec(propertyAttrs); 137 | if (!parsedPropertyAttributes) { 138 | continue; 139 | } 140 | 141 | const dataTypeString: string = parsedPropertyAttributes[1]; 142 | const checkDataType: [DataType, Uri] = DataType.getDataTypeAndUri(dataTypeString, document.uri); 143 | if (checkDataType) { 144 | property.dataType = checkDataType[0]; 145 | if (checkDataType[1]) { 146 | property.dataTypeComponentUri = checkDataType[1]; 147 | } 148 | } 149 | property.name = parsedPropertyAttributes[2]; 150 | 151 | const removedName: string = propertyMatch[0].slice(0, -property.name.length); 152 | const nameAttributeOffset: number = propertyMatch.index + removedName.length; 153 | property.nameRange = new Range( 154 | document.positionAt(nameAttributeOffset), 155 | document.positionAt(nameAttributeOffset + property.name.length) 156 | ); 157 | 158 | const dataTypeOffset: number = propertyMatch.index + removedName.lastIndexOf(dataTypeString); 159 | property.dataTypeRange = new Range( 160 | document.positionAt(dataTypeOffset), 161 | document.positionAt(dataTypeOffset + dataTypeString.length) 162 | ); 163 | } 164 | 165 | if (property.name) { 166 | properties.set(property.name.toLowerCase(), property); 167 | } 168 | } 169 | 170 | return properties; 171 | } 172 | 173 | /** 174 | * Constructs the getter implicit function for the given component property 175 | * @param property The component property for which to construct the getter 176 | * @param componentUri The URI of the component in which the property is defined 177 | */ 178 | export function constructGetter(property: Property, componentUri: Uri): UserFunction { 179 | return { 180 | access: Access.Public, 181 | static: false, 182 | abstract: false, 183 | final: false, 184 | bodyRange: undefined, 185 | name: "get" + property.name.charAt(0).toUpperCase() + property.name.slice(1), 186 | description: property.description, 187 | returntype: property.dataType, 188 | returnTypeUri: property.dataTypeComponentUri, 189 | nameRange: property.nameRange, 190 | signatures: [{parameters: []}], 191 | location: new Location(componentUri, property.propertyRange), 192 | isImplicit: true 193 | }; 194 | } 195 | 196 | /** 197 | * Constructs the setter implicit function for the given component property 198 | * @param property The component property for which to construct the setter 199 | * @param componentUri The URI of the component in which the property is defined 200 | */ 201 | export function constructSetter(property: Property, componentUri: Uri): UserFunction { 202 | let implicitFunctionSignature: UserFunctionSignature = { 203 | parameters: [ 204 | { 205 | name: property.name, 206 | nameRange: undefined, 207 | description: property.description, 208 | required: true, 209 | dataType: property.dataType, 210 | dataTypeComponentUri: property.dataTypeComponentUri, 211 | default: property.default 212 | } 213 | ] 214 | }; 215 | 216 | return { 217 | access: Access.Public, 218 | static: false, 219 | abstract: false, 220 | final: false, 221 | bodyRange: undefined, 222 | name: "set" + property.name.charAt(0).toUpperCase() + property.name.slice(1), 223 | description: property.description, 224 | returntype: DataType.Component, 225 | returnTypeUri: componentUri, 226 | nameRange: property.nameRange, 227 | signatures: [implicitFunctionSignature], 228 | location: new Location(componentUri, property.propertyRange), 229 | isImplicit: true 230 | }; 231 | } 232 | -------------------------------------------------------------------------------- /src/features/signatureHelpProvider.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken, ParameterInformation, Position, Range, SignatureHelp, SignatureHelpProvider, SignatureInformation, TextDocument, Uri, workspace, WorkspaceConfiguration, SignatureHelpContext } from "vscode"; 2 | import { Component, objectNewInstanceInitPrefix } from "../entities/component"; 3 | import { DataType } from "../entities/dataType"; 4 | import { constructSyntaxString, Function, getScriptFunctionArgRanges } from "../entities/function"; 5 | import { Parameter, namedParameterPattern } from "../entities/parameter"; 6 | import { getVariableScopePrefixPattern, Scope, unscopedPrecedence } from "../entities/scope"; 7 | import { constructSignatureLabelParamsPrefix, getSignatureParamsLabelOffsetTuples, Signature } from "../entities/signature"; 8 | import { getFunctionFromPrefix, isUserFunctionVariable, UserFunction, UserFunctionVariable, variablesToUserFunctions } from "../entities/userFunction"; 9 | import { collectDocumentVariableAssignments, Variable } from "../entities/variable"; 10 | import { BackwardIterator, getPrecedingIdentifierRange, isContinuingExpression, getStartSigPosition as findStartSigPosition, getClosingPosition } from "../utils/contextUtil"; 11 | import { DocumentPositionStateContext, getDocumentPositionStateContext } from "../utils/documentUtil"; 12 | import { equalsIgnoreCase, textToMarkdownString } from "../utils/textUtil"; 13 | import * as cachedEntity from "./cachedEntities"; 14 | import { componentPathToUri, getComponent } from "./cachedEntities"; 15 | 16 | export default class CFMLSignatureHelpProvider implements SignatureHelpProvider { 17 | /** 18 | * Provide help for the signature at the given position and document. 19 | * @param document The document in which the command was invoked. 20 | * @param position The position at which the command was invoked. 21 | * @param _token A cancellation token. 22 | * @param _context Information about how signature help was triggered. 23 | */ 24 | public async provideSignatureHelp(document: TextDocument, position: Position, _token: CancellationToken, _context: SignatureHelpContext): Promise { 25 | const cfmlSignatureSettings: WorkspaceConfiguration = workspace.getConfiguration("cfml.signature", document.uri); 26 | if (!cfmlSignatureSettings.get("enable", true)) { 27 | return null; 28 | } 29 | 30 | const documentPositionStateContext: DocumentPositionStateContext = getDocumentPositionStateContext(document, position); 31 | 32 | if (documentPositionStateContext.positionInComment) { 33 | return null; 34 | } 35 | 36 | const sanitizedDocumentText: string = documentPositionStateContext.sanitizedDocumentText; 37 | 38 | let backwardIterator: BackwardIterator = new BackwardIterator(documentPositionStateContext, position); 39 | 40 | backwardIterator.next(); 41 | const iteratedSigPosition: Position = findStartSigPosition(backwardIterator); 42 | if (!iteratedSigPosition) { 43 | return null; 44 | } 45 | 46 | const startSigPosition: Position = document.positionAt(document.offsetAt(iteratedSigPosition) + 2); 47 | const endSigPosition: Position = getClosingPosition(documentPositionStateContext, document.offsetAt(startSigPosition), ")").translate(0, -1); 48 | const functionArgRanges: Range[] = getScriptFunctionArgRanges(documentPositionStateContext, new Range(startSigPosition, endSigPosition)); 49 | 50 | let paramIndex: number = 0; 51 | paramIndex = functionArgRanges.findIndex((range: Range) => { 52 | return range.contains(position); 53 | }); 54 | if (paramIndex === -1) { 55 | return null; 56 | } 57 | const paramText: string = sanitizedDocumentText.slice(document.offsetAt(functionArgRanges[paramIndex].start), document.offsetAt(functionArgRanges[paramIndex].end)); 58 | 59 | const startSigPositionPrefix: string = sanitizedDocumentText.slice(0, document.offsetAt(startSigPosition)); 60 | 61 | let entry: Function; 62 | 63 | // Check if initializing via "new" operator 64 | const objectNewInstanceInitPrefixMatch: RegExpExecArray = objectNewInstanceInitPrefix.exec(startSigPositionPrefix); 65 | if (objectNewInstanceInitPrefixMatch) { 66 | const componentDotPath: string = objectNewInstanceInitPrefixMatch[2]; 67 | const componentUri: Uri = componentPathToUri(componentDotPath, document.uri); 68 | if (componentUri) { 69 | const initComponent: Component = getComponent(componentUri); 70 | if (initComponent) { 71 | const initMethod: string = initComponent.initmethod ? initComponent.initmethod.toLowerCase() : "init"; 72 | if (initComponent.functions.has(initMethod)) { 73 | entry = initComponent.functions.get(initMethod); 74 | } 75 | } 76 | } 77 | } 78 | 79 | if (!entry) { 80 | let identWordRange: Range = getPrecedingIdentifierRange(documentPositionStateContext, backwardIterator.getPosition()); 81 | if (!identWordRange) { 82 | return null; 83 | } 84 | 85 | const ident: string = document.getText(identWordRange); 86 | const lowerIdent: string = ident.toLowerCase(); 87 | 88 | const startIdentPositionPrefix: string = sanitizedDocumentText.slice(0, document.offsetAt(identWordRange.start)); 89 | 90 | // Global function 91 | if (!isContinuingExpression(startIdentPositionPrefix)) { 92 | entry = cachedEntity.getGlobalFunction(lowerIdent); 93 | } 94 | 95 | // Check user functions 96 | if (!entry) { 97 | const userFun: UserFunction = await getFunctionFromPrefix(documentPositionStateContext, lowerIdent, startIdentPositionPrefix); 98 | 99 | // Ensure this does not trigger on script function definition 100 | if (userFun && userFun.location.uri === document.uri && userFun.location.range.contains(position) && (!userFun.bodyRange || !userFun.bodyRange.contains(position))) { 101 | return null; 102 | } 103 | 104 | entry = userFun; 105 | } 106 | 107 | // Check variables 108 | if (!entry) { 109 | const variableScopePrefixPattern: RegExp = getVariableScopePrefixPattern(); 110 | const variableScopePrefixMatch: RegExpExecArray = variableScopePrefixPattern.exec(startIdentPositionPrefix); 111 | if (variableScopePrefixMatch) { 112 | const scopePrefix: string = variableScopePrefixMatch[1]; 113 | let prefixScope: Scope; 114 | if (scopePrefix) { 115 | prefixScope = Scope.valueOf(scopePrefix); 116 | } 117 | const allDocumentVariableAssignments: Variable[] = collectDocumentVariableAssignments(documentPositionStateContext); 118 | const userFunctionVariables: UserFunctionVariable[] = allDocumentVariableAssignments.filter((variable: Variable) => { 119 | if (variable.dataType !== DataType.Function || !isUserFunctionVariable(variable) || !equalsIgnoreCase(variable.identifier, lowerIdent)) { 120 | return false; 121 | } 122 | 123 | if (prefixScope) { 124 | return (variable.scope === prefixScope || (variable.scope === Scope.Unknown && unscopedPrecedence.includes(prefixScope))); 125 | } 126 | 127 | return (unscopedPrecedence.includes(variable.scope) || variable.scope === Scope.Unknown); 128 | }).map((variable: Variable) => { 129 | return variable as UserFunctionVariable; 130 | }); 131 | const userFunctions: UserFunction[] = variablesToUserFunctions(userFunctionVariables); 132 | if (userFunctions.length > 0) { 133 | entry = userFunctions[0]; 134 | } 135 | } 136 | } 137 | } 138 | 139 | if (!entry) { 140 | return null; 141 | } 142 | 143 | let sigHelp = new SignatureHelp(); 144 | 145 | entry.signatures.forEach((signature: Signature, sigIndex: number) => { 146 | const sigDesc: string = signature.description ? signature.description : entry.description; 147 | const sigLabel: string = constructSyntaxString(entry, sigIndex); 148 | let signatureInfo = new SignatureInformation(sigLabel, textToMarkdownString(sigDesc)); 149 | 150 | const sigParamsPrefixLength: number = constructSignatureLabelParamsPrefix(entry).length + 1; 151 | const sigParamsLabelOffsetTuples: [number, number][] = getSignatureParamsLabelOffsetTuples(signature.parameters).map((val: [number, number]) => { 152 | return [val[0] + sigParamsPrefixLength, val[1] + sigParamsPrefixLength] as [number, number]; 153 | }); 154 | 155 | signatureInfo.parameters = signature.parameters.map((param: Parameter, paramIdx: number) => { 156 | let paramInfo: ParameterInformation = new ParameterInformation(sigParamsLabelOffsetTuples[paramIdx], textToMarkdownString(param.description)); 157 | return paramInfo; 158 | }); 159 | sigHelp.signatures.push(signatureInfo); 160 | }); 161 | 162 | sigHelp.activeSignature = 0; 163 | for (let i = 0; i < sigHelp.signatures.length; i++) { 164 | const currSig = sigHelp.signatures[i]; 165 | if (paramIndex < currSig.parameters.length) { 166 | sigHelp.activeSignature = i; 167 | break; 168 | } 169 | } 170 | 171 | // Consider named parameters 172 | let namedParamMatch: RegExpExecArray = null; 173 | if (namedParamMatch = namedParameterPattern.exec(paramText)) { 174 | // TODO: Consider argumentCollection 175 | const paramName: string = namedParamMatch[1]; 176 | const namedParamIndex: number = entry.signatures[sigHelp.activeSignature].parameters.findIndex((param: Parameter) => { 177 | return equalsIgnoreCase(paramName, param.name); 178 | }); 179 | if (namedParamIndex !== -1) { 180 | paramIndex = namedParamIndex; 181 | } 182 | } 183 | 184 | sigHelp.activeParameter = Math.min(paramIndex, sigHelp.signatures[sigHelp.activeSignature].parameters.length - 1); 185 | 186 | return sigHelp; 187 | } 188 | } 189 | 190 | 191 | -------------------------------------------------------------------------------- /src/features/typeDefinitionProvider.ts: -------------------------------------------------------------------------------- 1 | import { TypeDefinitionProvider, TextDocument, Position, CancellationToken, Definition, Range, Location } from "vscode"; 2 | import { Component } from "../entities/component"; 3 | import { getComponent } from "./cachedEntities"; 4 | import { Scope, getValidScopesPrefixPattern, getVariableScopePrefixPattern, unscopedPrecedence } from "../entities/scope"; 5 | import { UserFunction, UserFunctionSignature, Argument, getLocalVariables, getFunctionFromPrefix } from "../entities/userFunction"; 6 | import { Property } from "../entities/property"; 7 | import { equalsIgnoreCase } from "../utils/textUtil"; 8 | import { Variable, parseVariableAssignments, getApplicationVariables, getServerVariables } from "../entities/variable"; 9 | import { DocumentPositionStateContext, getDocumentPositionStateContext } from "../utils/documentUtil"; 10 | 11 | export default class CFMLTypeDefinitionProvider implements TypeDefinitionProvider { 12 | 13 | /** 14 | * Provide the type definition of the symbol at the given position in the given document. 15 | * @param document The document for which the command was invoked. 16 | * @param position The position for which the command was invoked. 17 | * @param _token A cancellation token. 18 | */ 19 | public async provideTypeDefinition(document: TextDocument, position: Position, _token: CancellationToken): Promise { 20 | const results: Definition = []; 21 | 22 | const documentPositionStateContext: DocumentPositionStateContext = getDocumentPositionStateContext(document, position); 23 | 24 | if (documentPositionStateContext.positionInComment) { 25 | return null; 26 | } 27 | 28 | const docIsCfcFile: boolean = documentPositionStateContext.isCfcFile; 29 | const docIsCfmFile: boolean = documentPositionStateContext.isCfmFile; 30 | let wordRange: Range = document.getWordRangeAtPosition(position); 31 | const currentWord: string = documentPositionStateContext.currentWord; 32 | const lowerCurrentWord: string = currentWord.toLowerCase(); 33 | if (!wordRange) { 34 | wordRange = new Range(position, position); 35 | } 36 | 37 | const docPrefix: string = documentPositionStateContext.docPrefix; 38 | 39 | if (docIsCfcFile) { 40 | const thisComponent: Component = documentPositionStateContext.component; 41 | if (thisComponent) { 42 | // Component functions (related) 43 | thisComponent.functions.forEach((func: UserFunction) => { 44 | // Argument declarations 45 | func.signatures.forEach((signature: UserFunctionSignature) => { 46 | signature.parameters.filter((arg: Argument) => { 47 | return arg.dataTypeComponentUri && arg.nameRange && arg.nameRange.contains(position); 48 | }).forEach((arg: Argument) => { 49 | const argTypeComp: Component = getComponent(arg.dataTypeComponentUri); 50 | if (argTypeComp) { 51 | results.push(new Location( 52 | argTypeComp.uri, 53 | argTypeComp.declarationRange 54 | )); 55 | } 56 | }); 57 | }); 58 | 59 | if (func.bodyRange && func.bodyRange.contains(position)) { 60 | // Local variable uses 61 | const localVariables = getLocalVariables(func, documentPositionStateContext, thisComponent.isScript); 62 | const localVarPrefixPattern = getValidScopesPrefixPattern([Scope.Local], true); 63 | if (localVarPrefixPattern.test(docPrefix)) { 64 | localVariables.filter((localVar: Variable) => { 65 | return position.isAfterOrEqual(localVar.declarationLocation.range.start) && equalsIgnoreCase(localVar.identifier, currentWord) && localVar.dataTypeComponentUri; 66 | }).forEach((localVar: Variable) => { 67 | const localVarTypeComp: Component = getComponent(localVar.dataTypeComponentUri); 68 | if (localVarTypeComp) { 69 | results.push(new Location( 70 | localVarTypeComp.uri, 71 | localVarTypeComp.declarationRange 72 | )); 73 | } 74 | }); 75 | } 76 | 77 | // Argument uses 78 | if (results.length === 0) { 79 | const argumentPrefixPattern = getValidScopesPrefixPattern([Scope.Arguments], true); 80 | if (argumentPrefixPattern.test(docPrefix)) { 81 | func.signatures.forEach((signature: UserFunctionSignature) => { 82 | signature.parameters.filter((arg: Argument) => { 83 | return equalsIgnoreCase(arg.name, currentWord) && arg.dataTypeComponentUri; 84 | }).forEach((arg: Argument) => { 85 | const argTypeComp: Component = getComponent(arg.dataTypeComponentUri); 86 | if (argTypeComp) { 87 | results.push(new Location( 88 | argTypeComp.uri, 89 | argTypeComp.declarationRange 90 | )); 91 | } 92 | }); 93 | }); 94 | } 95 | } 96 | } 97 | }); 98 | 99 | // Component properties (declarations) 100 | thisComponent.properties.filter((prop: Property) => { 101 | return prop.dataTypeComponentUri !== undefined && prop.nameRange.contains(position); 102 | }).forEach((prop: Property) => { 103 | let propTypeComp: Component = getComponent(prop.dataTypeComponentUri); 104 | if (propTypeComp) { 105 | results.push(new Location( 106 | propTypeComp.uri, 107 | propTypeComp.declarationRange 108 | )); 109 | } 110 | }); 111 | 112 | // Component variables 113 | const variablesPrefixPattern = getValidScopesPrefixPattern([Scope.Variables], false); 114 | if (variablesPrefixPattern.test(docPrefix)) { 115 | thisComponent.variables.filter((variable: Variable) => { 116 | return equalsIgnoreCase(variable.identifier, currentWord) && variable.dataTypeComponentUri; 117 | }).forEach((variable: Variable) => { 118 | const varTypeComp: Component = getComponent(variable.dataTypeComponentUri); 119 | if (varTypeComp) { 120 | results.push(new Location( 121 | varTypeComp.uri, 122 | varTypeComp.declarationRange 123 | )); 124 | } 125 | }); 126 | } 127 | } 128 | } else if (docIsCfmFile) { 129 | const docVariableAssignments: Variable[] = parseVariableAssignments(documentPositionStateContext, false); 130 | const variableScopePrefixPattern: RegExp = getVariableScopePrefixPattern(); 131 | const variableScopePrefixMatch: RegExpExecArray = variableScopePrefixPattern.exec(docPrefix); 132 | if (variableScopePrefixMatch) { 133 | const validScope: string = variableScopePrefixMatch[1]; 134 | let currentScope: Scope; 135 | if (validScope) { 136 | currentScope = Scope.valueOf(validScope); 137 | } 138 | 139 | docVariableAssignments.filter((variable: Variable) => { 140 | if (!equalsIgnoreCase(variable.identifier, currentWord) || !variable.dataTypeComponentUri) { 141 | return false; 142 | } 143 | 144 | if (currentScope) { 145 | return (variable.scope === currentScope || (variable.scope === Scope.Unknown && unscopedPrecedence.includes(currentScope))); 146 | } 147 | 148 | return (unscopedPrecedence.includes(variable.scope) || variable.scope === Scope.Unknown); 149 | }).forEach((variable: Variable) => { 150 | const varTypeComp: Component = getComponent(variable.dataTypeComponentUri); 151 | if (varTypeComp) { 152 | results.push(new Location( 153 | varTypeComp.uri, 154 | varTypeComp.declarationRange 155 | )); 156 | } 157 | }); 158 | } 159 | } 160 | 161 | // User functions 162 | const externalUserFunc: UserFunction = await getFunctionFromPrefix(documentPositionStateContext, lowerCurrentWord); 163 | if (externalUserFunc && externalUserFunc.returnTypeUri) { 164 | const returnTypeComponent: Component = getComponent(externalUserFunc.returnTypeUri); 165 | if (returnTypeComponent) { 166 | results.push(new Location( 167 | returnTypeComponent.uri, 168 | returnTypeComponent.declarationRange 169 | )); 170 | } 171 | } 172 | 173 | // Application variables 174 | const applicationVariablesPrefixPattern = getValidScopesPrefixPattern([Scope.Application, Scope.Session, Scope.Request], false); 175 | const variableScopePrefixMatch: RegExpExecArray = applicationVariablesPrefixPattern.exec(docPrefix); 176 | if (variableScopePrefixMatch) { 177 | const currentScope: string = Scope.valueOf(variableScopePrefixMatch[1]); 178 | 179 | const applicationDocVariables: Variable[] = await getApplicationVariables(document.uri); 180 | applicationDocVariables.filter((variable: Variable) => { 181 | return variable.scope === currentScope && equalsIgnoreCase(variable.identifier, currentWord) && variable.dataTypeComponentUri; 182 | }).forEach((variable: Variable) => { 183 | const varTypeComp: Component = getComponent(variable.dataTypeComponentUri); 184 | if (varTypeComp) { 185 | results.push(new Location( 186 | varTypeComp.uri, 187 | varTypeComp.declarationRange 188 | )); 189 | } 190 | }); 191 | } 192 | 193 | // Server variables 194 | const serverVariablesPrefixPattern = getValidScopesPrefixPattern([Scope.Server], false); 195 | if (serverVariablesPrefixPattern.test(docPrefix)) { 196 | const serverDocVariables: Variable[] = getServerVariables(document.uri); 197 | serverDocVariables.filter((variable: Variable) => { 198 | return variable.scope === Scope.Server && equalsIgnoreCase(variable.identifier, currentWord) && variable.dataTypeComponentUri; 199 | }).forEach((variable: Variable) => { 200 | const varTypeComp: Component = getComponent(variable.dataTypeComponentUri); 201 | if (varTypeComp) { 202 | results.push(new Location( 203 | varTypeComp.uri, 204 | varTypeComp.declarationRange 205 | )); 206 | } 207 | }); 208 | } 209 | 210 | return results; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/entities/scope.ts: -------------------------------------------------------------------------------- 1 | export enum Scope { 2 | Application = "application", 3 | Arguments = "arguments", 4 | Attributes = "attributes", 5 | Caller = "caller", 6 | Cffile = "cffile", 7 | CGI = "cgi", 8 | Client = "client", 9 | Cookie = "cookie", 10 | Flash = "flash", 11 | Form = "form", 12 | Local = "local", 13 | Request = "request", 14 | Server = "server", 15 | Session = "session", 16 | Static = "static", 17 | This = "this", 18 | ThisTag = "thistag", 19 | Thread = "thread", 20 | ThreadLocal = "threadlocal", // Not a real prefix 21 | URL = "url", 22 | Unknown = "unknown", // Not a real scope. Use as default. 23 | Variables = "variables" 24 | } 25 | 26 | export namespace Scope { 27 | /** 28 | * Resolves a string value of scope to an enumeration member 29 | * @param scope The scope string to resolve 30 | */ 31 | export function valueOf(scope: string): Scope { 32 | switch (scope.toLowerCase()) { 33 | case "application": 34 | return Scope.Application; 35 | case "arguments": 36 | return Scope.Arguments; 37 | case "attributes": 38 | return Scope.Attributes; 39 | case "caller": 40 | return Scope.Caller; 41 | case "cffile": 42 | return Scope.Cffile; 43 | case "cgi": 44 | return Scope.CGI; 45 | case "client": 46 | return Scope.Client; 47 | case "cookie": 48 | return Scope.Cookie; 49 | case "flash": 50 | return Scope.Flash; 51 | case "form": 52 | return Scope.Form; 53 | case "local": 54 | return Scope.Local; 55 | case "request": 56 | return Scope.Request; 57 | case "server": 58 | return Scope.Server; 59 | case "session": 60 | return Scope.Session; 61 | case "static": 62 | return Scope.Static; 63 | case "this": 64 | return Scope.This; 65 | case "thistag": 66 | return Scope.ThisTag; 67 | case "thread": 68 | return Scope.Thread; 69 | case "url": 70 | return Scope.URL; 71 | case "variables": 72 | return Scope.Variables; 73 | default: 74 | return Scope.Unknown; 75 | } 76 | } 77 | } 78 | 79 | export const allScopes: Scope[] = [ 80 | Scope.Application, 81 | Scope.Arguments, 82 | Scope.Attributes, 83 | Scope.Caller, 84 | Scope.Cffile, 85 | Scope.CGI, 86 | Scope.Client, 87 | Scope.Cookie, 88 | Scope.Flash, 89 | Scope.Form, 90 | Scope.Local, 91 | Scope.Request, 92 | Scope.Server, 93 | Scope.Session, 94 | Scope.Static, 95 | Scope.This, 96 | Scope.ThisTag, 97 | Scope.Thread, 98 | Scope.URL, 99 | Scope.Variables 100 | ]; 101 | 102 | export const unscopedPrecedence: Scope[] = [ 103 | Scope.Local, 104 | Scope.Arguments, 105 | Scope.ThreadLocal, 106 | // Query (not a true scope; variables in query loops) 107 | Scope.Thread, 108 | Scope.Variables, 109 | Scope.CGI, 110 | Scope.Cffile, 111 | Scope.URL, 112 | Scope.Form, 113 | Scope.Cookie, 114 | Scope.Client 115 | ]; 116 | 117 | interface ScopeDetails { 118 | detail: string; 119 | description: string; 120 | prefixRequired: boolean; 121 | } 122 | 123 | interface Scopes { 124 | [scope: string]: ScopeDetails; 125 | } 126 | 127 | export const scopes: Scopes = { 128 | "application": { 129 | detail: "(scope) application", 130 | description: "Contains variables that are associated with one, named application on a server. The cfapplication tag name attribute or the Application.cfc This.name variable setting specifies the application name.", 131 | prefixRequired: true 132 | }, 133 | "arguments": { 134 | detail: "(scope) arguments", 135 | description: "Variables passed in a call to a user-defined function or ColdFusion component method.", 136 | prefixRequired: false 137 | }, 138 | "attributes": { 139 | detail: "(scope) attributes", 140 | description: "Used only in custom tag pages and threads. Contains the values passed by the calling page or cfthread tag in the tag's attributes.", 141 | prefixRequired: true 142 | }, 143 | "caller": { 144 | detail: "(scope) caller", 145 | description: "Used only in custom tag pages. The custom tag's Caller scope is a reference to the calling page's Variables scope. Any variables that you create or change in the custom tag page using the Caller scope are visible in the calling page's Variables scope.", 146 | prefixRequired: false 147 | }, 148 | "cffile": { 149 | detail: "(scope) cffile", 150 | description: "Used to access the properties of a cffile object after an invocation of cffile.", 151 | prefixRequired: true 152 | }, 153 | "cgi": { 154 | detail: "(scope) cgi", 155 | description: "Contains environment variables identifying the context in which a page was requested. The variables available depend on the browser and server software.", 156 | prefixRequired: true 157 | }, 158 | "client": { 159 | detail: "(scope) client", 160 | description: "Contains variables that are associated with one client. Client variables let you maintain state as a user moves from page to page in an application, and are available across browser sessions. By default, Client variables are stored in the system registry, but you can store them in a cookie or a database. Client variables cannot be complex data types and can include periods in their names.", 161 | prefixRequired: false 162 | }, 163 | "cookie": { 164 | detail: "(scope) cookie", 165 | description: "Contains variables maintained in a user's browser as cookies. Cookies are typically stored in a file on the browser, so they are available across browser sessions and applications. You can create memory-only Cookie variables, which are not available after the user closes the browser. Cookie scope variable names can include periods.", 166 | prefixRequired: false 167 | }, 168 | "flash": { 169 | detail: "(scope) flash", 170 | description: "Variables sent by a SWF movie to ColdFusion and returned by ColdFusion to the movie. For more information, see Using the Flash Remoting Service.", 171 | prefixRequired: true 172 | }, 173 | "form": { 174 | detail: "(scope) form", 175 | description: "Contains variables passed from a Form page to its action page as the result of submitting the form. (If you use the HTML form tag, you must use post method.)", 176 | prefixRequired: false 177 | }, 178 | "local": { 179 | detail: "(scope) local", 180 | description: "Contains variables that are declared inside a user-defined function or ColdFusion component method and exist only while a function executes.", 181 | prefixRequired: false 182 | }, 183 | "request": { 184 | detail: "(scope) request", 185 | description: "Used to hold data that must be available for the duration of one HTTP request. The Request scope is available to all pages, including custom tags and nested custom tags, that are processed in response to the request. This scope is useful for nested (child/parent) tags. This scope can often be used in place of the Application scope, to avoid the need for locking variables.", 186 | prefixRequired: true 187 | }, 188 | "server": { 189 | detail: "(scope) server", 190 | description: "Contains variables that are associated with the current ColdFusion server. This scope lets you define variables that are available to all your ColdFusion pages, across multiple applications.", 191 | prefixRequired: true 192 | }, 193 | "session": { 194 | detail: "(scope) session", 195 | description: "Contains variables that are associated with one client and persist only as long as the client maintains a session. They are stored in the server's memory and can be set to time out after a period of inactivity.", 196 | prefixRequired: true 197 | }, 198 | // Lucee-only 199 | "static": { 200 | detail: "(scope) static", 201 | description: "(Lucee-only) For use with functions and variables within a ColdFusion component that do not belong to an instantiated object.", 202 | prefixRequired: true 203 | }, 204 | "this": { 205 | detail: "(scope) this", 206 | description: "Exists only in ColdFusion components or cffunction tags that are part of a containing object such as a ColdFusion Struct. Exists for the duration of the component instance or containing object. Data in the This scope is accessible from outside the component or container by using the instance or object name as a prefix.", 207 | prefixRequired: true 208 | }, 209 | "thisTag": { 210 | detail: "(scope) thisTag", 211 | description: "Used only in custom tag pages. The ThisTag scope is active for the current invocation of the tag. If a custom tag contains a nested tag, any ThisTag scope values you set before calling the nested tag are preserved when the nested tag returns to the calling tag. The ThisTag scope includes three built-in variables that identify the tag's execution mode, contain the tag's generated contents, and indicate whether the tag has an end tag.A nested custom tag can use the cfassociate tag to return values to the calling tag's ThisTag scope.", 212 | prefixRequired: true 213 | }, 214 | "thread": { 215 | detail: "(scope) thread", 216 | description: "Variables that are created and changed inside a ColdFusion thread, but can be read by all code on the page that creates the thread. Each thread has a Thread scope that is a subscope of a cfthread scope.", 217 | prefixRequired: false 218 | }, 219 | "url": { 220 | detail: "(scope) url", 221 | description: "Contains parameters passed to the current page in the URL that is used to call it. The parameters are appended to the URL in the format ?variablename1=value&variablename2=value...", 222 | prefixRequired: false 223 | }, 224 | "variables": { 225 | detail: "(scope) variables", 226 | description: "The default scope for variables of any type that are created with the cfset and cfparam tags. A Variables scope variable is available only on the page on which it is created and any included pages (see also the Caller scope). Variables scope variables created in a CFC are available only to the component and its functions, and not to the page that instantiates the component or calls its functions.", 227 | prefixRequired: false 228 | }, 229 | }; 230 | 231 | /** 232 | * Returns a regular expression that optionally captures a valid scope 233 | * @param scopes An array of scopes to include 234 | * @param optionalScope Whether the scope is optional 235 | */ 236 | export function getValidScopesPrefixPattern(scopes: Scope[], optionalScope: boolean = true) { 237 | const validScopes: string = scopes.join("|"); 238 | let pattern: string = `(?:^|[^.\\s])\\s*(?:\\b(${validScopes})\\s*\\.\\s*)`; 239 | if (optionalScope) { 240 | pattern += "?"; 241 | } 242 | 243 | return new RegExp(pattern + "$", "i"); 244 | } 245 | 246 | /** 247 | * Returns a regular expression that matches a scoped variable 248 | */ 249 | export function getVariableScopePrefixPattern() { 250 | return getValidScopesPrefixPattern(allScopes, true); 251 | } 252 | -------------------------------------------------------------------------------- /resources/schemas/cfdocs.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "definitions": { 4 | "engineDetail": { 5 | "title": "Engine Detail", 6 | "type": "object", 7 | "properties": { 8 | "minimum_version": { 9 | "title": "Minimum version", 10 | "description": "The version of the engine in which this was introduced", 11 | "type": "string", 12 | "pattern": "^((0|[1-9]\\d*)(\\.(0|[1-9]\\d*)(\\.(0|[1-9]\\d*))?)?)?$" 13 | }, 14 | "deprecated": { 15 | "title": "Deprecated version", 16 | "description": "The version of the engine in which this was deprecated", 17 | "type": "string", 18 | "pattern": "^((0|[1-9]\\d*)(\\.(0|[1-9]\\d*)(\\.(0|[1-9]\\d*))?)?)?$", 19 | "default": "" 20 | }, 21 | "removed": { 22 | "title": "Removed version", 23 | "description": "The version of the engine in which this was removed", 24 | "type": "string", 25 | "pattern": "^((0|[1-9]\\d*)(\\.(0|[1-9]\\d*)(\\.(0|[1-9]\\d*))?)?)?$", 26 | "default": "" 27 | }, 28 | "notes": { 29 | "title": "Notes", 30 | "description": "Additional notes about engine compatibility", 31 | "type": "string", 32 | "default": "" 33 | }, 34 | "docs": { 35 | "title": "Docs", 36 | "description": "Link to engine-specific documentation", 37 | "type": "string", 38 | "default": "" 39 | } 40 | }, 41 | "required": [ 42 | "minimum_version" 43 | ] 44 | }, 45 | "engines": { 46 | "title": "Engines", 47 | "type": "object", 48 | "properties": { 49 | "coldfusion": { 50 | "title": "ColdFusion", 51 | "description": "Engine details for ColdFusion", 52 | "type": "object", 53 | "$ref": "#/definitions/engineDetail" 54 | }, 55 | "railo": { 56 | "title": "Railo", 57 | "description": "Engine details for Railo", 58 | "type": "object", 59 | "$ref": "#/definitions/engineDetail" 60 | }, 61 | "lucee": { 62 | "title": "Lucee", 63 | "description": "Engine details for Lucee", 64 | "type": "object", 65 | "$ref": "#/definitions/engineDetail" 66 | }, 67 | "openbd": { 68 | "title": "OpenBD", 69 | "description": "Engine details for OpenBD", 70 | "type": "object", 71 | "$ref": "#/definitions/engineDetail" 72 | } 73 | } 74 | }, 75 | "params": { 76 | "title": "Parameters", 77 | "description": "List of parameters", 78 | "type": "array", 79 | "minItems": 0, 80 | "items": { 81 | "title": "Parameter", 82 | "description": "Argument or attribute", 83 | "type": "object", 84 | "properties": { 85 | "name": { 86 | "title": "Name", 87 | "description": "Parameter name", 88 | "type": "string" 89 | }, 90 | "description": { 91 | "title": "Description", 92 | "description": "Description of the parameter", 93 | "type": "string" 94 | }, 95 | "type": { 96 | "title": "Type", 97 | "description": "The data type of the parameter", 98 | "type": "string", 99 | "enum": [ 100 | "any", 101 | "array", 102 | "binary", 103 | "boolean", 104 | "component", 105 | "date", 106 | "function", 107 | "guid", 108 | "numeric", 109 | "query", 110 | "string", 111 | "struct", 112 | "uuid", 113 | "variableName", 114 | "xml" 115 | ] 116 | }, 117 | "required": { 118 | "title": "Required", 119 | "description": "Whether this parameter is required", 120 | "type": "boolean" 121 | }, 122 | "default": { 123 | "title": "Default", 124 | "description": "The default value of the parameter when not provided", 125 | "type": "string", 126 | "default": "" 127 | }, 128 | "values": { 129 | "title": "Values", 130 | "description": "List of enumerated values for the parameter", 131 | "type": "array", 132 | "minItems": 0, 133 | "items": { 134 | "title": "Value", 135 | "description": "One of the enumerated values for the parameter", 136 | "type": "string" 137 | }, 138 | "default": [] 139 | }, 140 | "engines": { 141 | "title": "Engines", 142 | "description": "Engine compatibility information", 143 | "type": "object", 144 | "$ref": "#/definitions/engines" 145 | }, 146 | "callback_params": { 147 | "title": "Callback Parameters", 148 | "description": "When type is function, this ", 149 | "type": "array", 150 | "$ref": "#/definitions/params", 151 | "minItems": 0, 152 | "default": [] 153 | } 154 | }, 155 | "required": [ 156 | "name", 157 | "description", 158 | "type", 159 | "required" 160 | ] 161 | }, 162 | "default": [] 163 | } 164 | }, 165 | "title": "CFDocs", 166 | "description": "CFDocs data file", 167 | "type": "object", 168 | "properties": { 169 | "name": { 170 | "title": "Name", 171 | "description": "The name of the tag or function. Use lowercase.", 172 | "type": "string", 173 | "pattern": "^[a-z]\\w+$" 174 | }, 175 | "description": { 176 | "title": "Description", 177 | "description": "A short description of the item", 178 | "type": "string" 179 | }, 180 | "type": { 181 | "title": "Type", 182 | "description": "Whether this refers to a function, tag, or listing", 183 | "type": "string", 184 | "enum": [ 185 | "function", 186 | "tag", 187 | "listing" 188 | ] 189 | }, 190 | "syntax": { 191 | "title": "Syntax", 192 | "description": "The basic syntax of the tag or function", 193 | "type": "string", 194 | "default": "" 195 | }, 196 | "script": { 197 | "title": "Syntax", 198 | "description": "For tags, shows how the tag would be invoked from cfscript.", 199 | "type": "string", 200 | "default": "" 201 | }, 202 | "member": { 203 | "title": "Member", 204 | "description": "For functions, shows the available member function syntax.", 205 | "type": "string", 206 | "default": "" 207 | }, 208 | "returns": { 209 | "title": "Return Type", 210 | "description": "The return type of a function.", 211 | "type": "string", 212 | "enum": [ 213 | "any", 214 | "array", 215 | "binary", 216 | "boolean", 217 | "date", 218 | "function", 219 | "guid", 220 | "numeric", 221 | "query", 222 | "string", 223 | "struct", 224 | "uuid", 225 | "variableName", 226 | "void", 227 | "xml" 228 | ], 229 | "default": "void" 230 | }, 231 | "related": { 232 | "title": "Related", 233 | "description": "A list of tag or function names that are related to this item", 234 | "type": "array", 235 | "minItems": 0, 236 | "items": { 237 | "title": "Entity name", 238 | "description": "The name of a function or tag that is already documented", 239 | "type": "string" 240 | }, 241 | "default": [] 242 | }, 243 | "discouraged": { 244 | "title": "Discouraged", 245 | "description": "If this key exists and has content a warning is displayed stating that the tag or function is discouraged by the CFML community.", 246 | "type": "string", 247 | "default": "" 248 | }, 249 | "params": { 250 | "title": "Parameters", 251 | "description": "List of parameters", 252 | "type": "array", 253 | "$ref": "#/definitions/params", 254 | "minItems": 0, 255 | "default": [] 256 | }, 257 | "engines": { 258 | "title": "Engines", 259 | "description": "Engine compatibility information", 260 | "type": "object", 261 | "$ref": "#/definitions/engines" 262 | }, 263 | "links": { 264 | "title": "Links", 265 | "description": "A list of external references", 266 | "type": "array", 267 | "minItems": 0, 268 | "items": { 269 | "title": "Link", 270 | "description": "Information about the reference", 271 | "type": "object", 272 | "properties": { 273 | "title": { 274 | "title": "Title", 275 | "description": "A title for the link", 276 | "type": "string" 277 | }, 278 | "description": { 279 | "title": "Description", 280 | "description": "A description for the link", 281 | "type": "string" 282 | }, 283 | "url": { 284 | "title": "URL", 285 | "description": "The link URL", 286 | "type": "string" 287 | } 288 | }, 289 | "required": [ 290 | "title", 291 | "description", 292 | "url" 293 | ] 294 | }, 295 | "default": [] 296 | }, 297 | "examples": { 298 | "title": "Examples", 299 | "description": "A list of examples", 300 | "type": "array", 301 | "minItems": 0, 302 | "items": { 303 | "title": "Example", 304 | "description": "Information about the example", 305 | "type": "object", 306 | "properties": { 307 | "title": { 308 | "title": "Title", 309 | "description": "Name of the code example", 310 | "type": "string" 311 | }, 312 | "description": { 313 | "title": "Description", 314 | "description": "Description of the code example", 315 | "type": "string" 316 | }, 317 | "code": { 318 | "title": "Code", 319 | "description": "The example code", 320 | "type": "string" 321 | }, 322 | "result": { 323 | "title": "Result", 324 | "description": "The expected output of the code example", 325 | "type": "string" 326 | }, 327 | "runnable": { 328 | "title": "Runnable", 329 | "description": "Whether the code is runnable", 330 | "type": "boolean" 331 | } 332 | }, 333 | "required": [ 334 | "title", 335 | "description", 336 | "code" 337 | ] 338 | }, 339 | "default": [] 340 | } 341 | }, 342 | "required": [ 343 | "name", 344 | "description", 345 | "type" 346 | ] 347 | } 348 | -------------------------------------------------------------------------------- /src/utils/cfdocs/definitionInfo.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from "../../entities/dataType"; 2 | import { GlobalFunction, GlobalTag } from "../../entities/globals"; 3 | import { Parameter } from "../../entities/parameter"; 4 | import { Signature } from "../../entities/signature"; 5 | import { equalsIgnoreCase } from "../textUtil"; 6 | import CFDocsService from "./cfDocsService"; 7 | import { CFMLEngine, CFMLEngineName } from "./cfmlEngine"; 8 | import { multiSigGlobalFunctions } from "./multiSignatures"; 9 | import * as htmlEntities from "html-entities"; 10 | 11 | export interface Param { 12 | name: string; 13 | type: string; 14 | required: boolean; 15 | description?: string; 16 | default?: string; 17 | values?: string[]; 18 | } 19 | 20 | export interface EngineCompatibilityDetail { 21 | minimum_version?: string; 22 | deprecated?: string; 23 | removed?: string; 24 | notes?: string; 25 | docs?: string; 26 | } 27 | 28 | export interface EngineInfo { 29 | // expected to be CFMLEngineName 30 | [name: string]: EngineCompatibilityDetail; 31 | } 32 | 33 | export interface Example { 34 | title: string; 35 | description: string; 36 | code: string; 37 | result: string; 38 | runnable?: boolean; 39 | } 40 | 41 | /** 42 | * Resolves a string value of data type to an enumeration member 43 | * @param type The data type string to resolve 44 | */ 45 | function getParamDataType(type: string): DataType { 46 | switch (type) { 47 | case "any": 48 | return DataType.Any; 49 | case "array": 50 | return DataType.Array; 51 | case "binary": 52 | return DataType.Binary; 53 | case "boolean": 54 | return DataType.Boolean; 55 | case "component": 56 | return DataType.Component; 57 | case "date": 58 | return DataType.Date; 59 | case "function": 60 | return DataType.Function; 61 | case "guid": 62 | return DataType.GUID; 63 | case "numeric": 64 | return DataType.Numeric; 65 | case "query": 66 | return DataType.Query; 67 | case "string": 68 | return DataType.String; 69 | case "struct": 70 | return DataType.Struct; 71 | case "uuid": 72 | return DataType.UUID; 73 | case "variablename": 74 | return DataType.VariableName; 75 | case "xml": 76 | return DataType.XML; 77 | default: 78 | // console.log("Unknown param type: " + type); 79 | return DataType.Any; 80 | } 81 | } 82 | 83 | /** 84 | * Resolves a string value of data type to an enumeration member 85 | * @param type The data type string to resolve 86 | */ 87 | function getReturnDataType(type: string): DataType { 88 | switch (type) { 89 | case "any": 90 | return DataType.Any; 91 | case "array": 92 | return DataType.Array; 93 | case "binary": 94 | return DataType.Binary; 95 | case "boolean": 96 | return DataType.Boolean; 97 | case "date": 98 | return DataType.Date; 99 | case "function": 100 | return DataType.Function; 101 | case "guid": 102 | return DataType.GUID; 103 | case "numeric": 104 | return DataType.Numeric; 105 | case "query": 106 | return DataType.Query; 107 | case "string": 108 | return DataType.String; 109 | case "struct": 110 | return DataType.Struct; 111 | case "uuid": 112 | return DataType.UUID; 113 | case "variablename": 114 | return DataType.VariableName; 115 | case "void": 116 | return DataType.Void; 117 | case "xml": 118 | return DataType.XML; 119 | default: 120 | return DataType.Any; // DataType.Void? 121 | } 122 | } 123 | 124 | export class CFDocsDefinitionInfo { 125 | private static allFunctionNames: string[]; 126 | private static allTagNames: string[]; 127 | 128 | public name: string; 129 | public type: string; 130 | public syntax: string; 131 | public member?: string; 132 | public script?: string; 133 | public returns?: string; 134 | public related?: string[]; 135 | public description?: string; 136 | public discouraged?: string; 137 | public params?: Param[]; 138 | public engines?: EngineInfo; 139 | public links?: string[]; 140 | public examples?: Example[]; 141 | 142 | constructor( 143 | name: string, type: string, syntax: string, member: string, script: string, returns: string, related: string[], 144 | description: string, discouraged: string, params: Param[], engines: EngineInfo, links: string[], examples: Example[] 145 | ) { 146 | this.name = name; 147 | this.type = type; 148 | this.syntax = syntax; 149 | this.member = member; 150 | this.script = script; 151 | this.returns = returns; 152 | this.related = related; 153 | this.description = description; 154 | this.discouraged = discouraged; 155 | this.params = params; 156 | this.engines = engines; 157 | this.links = links; 158 | this.examples = examples; 159 | } 160 | 161 | /** 162 | * Returns whether this object is a function 163 | */ 164 | public isFunction(): boolean { 165 | return (equalsIgnoreCase(this.type, "function")); 166 | } 167 | 168 | /** 169 | * Returns whether this object is a tag 170 | */ 171 | public isTag(): boolean { 172 | return (equalsIgnoreCase(this.type, "tag")); 173 | } 174 | 175 | /** 176 | * Returns a GlobalFunction object based on this object 177 | */ 178 | public toGlobalFunction(): GlobalFunction { 179 | let signatures: Signature[] = []; 180 | if (multiSigGlobalFunctions.has(this.name)) { 181 | let thisMultiSigs: string[][] = multiSigGlobalFunctions.get(this.name); 182 | thisMultiSigs.forEach((thisMultiSig: string[]) => { 183 | let parameters: Parameter[] = []; 184 | thisMultiSig.forEach((multiSigParam: string) => { 185 | let paramFound = false; 186 | for (const param of this.params) { 187 | let multiSigParamParsed: string = multiSigParam.split("=")[0]; 188 | if (param.name === multiSigParamParsed) { 189 | let parameter: Parameter = { 190 | name: multiSigParam, 191 | dataType: getParamDataType(param.type.toLowerCase()), 192 | required: param.required, 193 | description: param.description, 194 | default: param.default, 195 | enumeratedValues: param.values 196 | }; 197 | parameters.push(parameter); 198 | paramFound = true; 199 | break; 200 | } 201 | } 202 | if (!paramFound) { 203 | let parameter: Parameter = { 204 | name: multiSigParam, 205 | dataType: DataType.Any, 206 | required: false, 207 | description: "" 208 | }; 209 | parameters.push(parameter); 210 | } 211 | }); 212 | let signatureInfo: Signature = { 213 | parameters: parameters 214 | }; 215 | signatures.push(signatureInfo); 216 | }); 217 | } else { 218 | let parameters: Parameter[] = this.params.map((param: Param) => { 219 | return { 220 | name: param.name, 221 | dataType: getParamDataType(param.type.toLowerCase()), 222 | required: param.required, 223 | description: htmlEntities.decode(param.description), 224 | default: param.default, 225 | enumeratedValues: param.values 226 | }; 227 | }); 228 | let signatureInfo: Signature = { 229 | parameters: parameters 230 | }; 231 | signatures.push(signatureInfo); 232 | } 233 | 234 | return { 235 | name: this.name, 236 | syntax: this.syntax, 237 | description: (this.description ? htmlEntities.decode(this.description) : ""), 238 | returntype: getReturnDataType(this.returns.toLowerCase()), 239 | signatures: signatures 240 | }; 241 | } 242 | 243 | /** 244 | * Returns a GlobalTag object based on this object 245 | */ 246 | public toGlobalTag(): GlobalTag { 247 | let parameters: Parameter[] = this.params.map((param: Param) => { 248 | return { 249 | name: param.name, 250 | dataType: getParamDataType(param.type.toLowerCase()), 251 | required: param.required, 252 | description: htmlEntities.decode(param.description), 253 | default: param.default, 254 | enumeratedValues: param.values 255 | }; 256 | }); 257 | 258 | let signatureInfo: Signature = { 259 | parameters: parameters 260 | }; 261 | let signatures: Signature[] = []; 262 | signatures.push(signatureInfo); 263 | 264 | return { 265 | name: this.name, 266 | syntax: this.syntax, 267 | scriptSyntax: this.script, 268 | description: (this.description ? htmlEntities.decode(this.description) : ""), 269 | signatures: signatures, 270 | hasBody: true 271 | }; 272 | } 273 | 274 | /** 275 | * Checks if this definition is compatible with given engine 276 | * @param engine The CFML engine with which to check compatibility 277 | */ 278 | public isCompatible(engine: CFMLEngine): boolean { 279 | const engineVendor: CFMLEngineName = engine.getName(); 280 | if (engineVendor === CFMLEngineName.Unknown || !this.engines) { 281 | return true; 282 | } 283 | 284 | const engineCompat: EngineCompatibilityDetail = this.engines[engineVendor]; 285 | if (!engineCompat) { 286 | return false; 287 | } 288 | 289 | const engineVersion: string = engine.getVersion(); 290 | if (!engineVersion) { 291 | return true; 292 | } 293 | 294 | if (engineCompat.minimum_version) { 295 | const minEngine: CFMLEngine = new CFMLEngine(engineVendor, engineCompat.minimum_version); 296 | if (engine.isOlder(minEngine)) { 297 | return false; 298 | } 299 | } 300 | 301 | if (engineCompat.removed) { 302 | const maxEngine: CFMLEngine = new CFMLEngine(engineVendor, engineCompat.removed); 303 | if (engine.isNewerOrEquals(maxEngine)) { 304 | return false; 305 | } 306 | } 307 | 308 | return true; 309 | } 310 | 311 | /** 312 | * Gets all function names documented by CFDocs. Once retrieved, they are statically stored. 313 | */ 314 | public static async getAllFunctionNames(): Promise { 315 | if (!CFDocsDefinitionInfo.allFunctionNames) { 316 | CFDocsDefinitionInfo.allFunctionNames = await CFDocsService.getAllFunctionNames(); 317 | } 318 | 319 | return CFDocsDefinitionInfo.allFunctionNames; 320 | } 321 | 322 | /** 323 | * Gets all tag names documented by CFDocs. Once retrieved, they are statically stored. 324 | */ 325 | public static async getAllTagNames(): Promise { 326 | if (!CFDocsDefinitionInfo.allTagNames) { 327 | CFDocsDefinitionInfo.allTagNames = await CFDocsService.getAllTagNames(); 328 | } 329 | 330 | return CFDocsDefinitionInfo.allTagNames; 331 | } 332 | 333 | /** 334 | * Returns whether the given identifier is the name of a function documented in CFDocs 335 | * @param name The identifier to check for 336 | */ 337 | public static async isFunctionName(name: string): Promise { 338 | let allFunctionNames: string[] = await CFDocsDefinitionInfo.getAllFunctionNames(); 339 | return allFunctionNames.includes(name.toLowerCase()); 340 | } 341 | 342 | /** 343 | * Returns whether the given identifier is the name of a tag documented in CFDocs 344 | * @param name The identifier to check for 345 | */ 346 | public static async isTagName(name: string): Promise { 347 | let allTagNames: string[] = await CFDocsDefinitionInfo.getAllTagNames(); 348 | return allTagNames.includes(name.toLowerCase()); 349 | } 350 | 351 | /** 352 | * Returns whether the given identifier is the name of a function or tag documented in CFDocs 353 | * @param name The identifier to check for 354 | */ 355 | public static async isIdentifier(name: string): Promise { 356 | return (CFDocsDefinitionInfo.isFunctionName(name) || CFDocsDefinitionInfo.isTagName(name)); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/features/colorProvider.ts: -------------------------------------------------------------------------------- 1 | 2 | import { DocumentColorProvider, TextDocument, CancellationToken, Range, Color, ColorPresentation, TextEdit, ColorInformation } from "vscode"; 3 | import { getCssRanges, isInCfOutput } from "../utils/contextUtil"; 4 | import { getDocumentStateContext, DocumentStateContext } from "../utils/documentUtil"; 5 | import { cssPropertyPattern } from "../entities/css/property"; 6 | import { cssDataManager, cssColors } from "../entities/css/languageFacts"; 7 | import { IPropertyData } from "../entities/css/cssLanguageTypes"; 8 | 9 | const rgbHexPattern = /#?#([0-9A-F]{3,4}|[0-9A-F]{6}|[0-9A-F]{8})\b/gi; 10 | const rgbFuncPattern = /\brgba?\s*\(\s*([0-9%.]+)\s*,?\s*([0-9%.]+)\s*,?\s*([0-9%.]+)(?:\s*(?:,|\/)?\s*([0-9%.]+)\s*)?\)/gi; 11 | const hslFuncPattern = /\bhsla?\s*\(\s*([0-9.]+)(deg|rad|grad|turn)?\s*,?\s*([0-9%.]+)\s*,?\s*([0-9%.]+)(?:\s*(?:,|\/)?\s*([0-9%.]+)\s*)?\)/gi; 12 | const colorKeywordPattern = new RegExp(`(^|\\s+)(${Object.keys(cssColors).join("|")})(?:\\s+|$)`, "gi"); 13 | 14 | export default class CFMLDocumentColorProvider implements DocumentColorProvider { 15 | 16 | /** 17 | * Provide colors for the given document. 18 | * @param document The document for which to provide the colors 19 | * @param _token A cancellation token 20 | */ 21 | public async provideDocumentColors(document: TextDocument, _token: CancellationToken): Promise { 22 | let result: ColorInformation[] = []; 23 | 24 | const documentStateContext: DocumentStateContext = getDocumentStateContext(document); 25 | const cssRanges: Range[] = getCssRanges(documentStateContext); 26 | 27 | for (const cssRange of cssRanges) { 28 | const rangeTextOffset: number = document.offsetAt(cssRange.start); 29 | const rangeText: string = documentStateContext.sanitizedDocumentText.slice(rangeTextOffset, document.offsetAt(cssRange.end)); 30 | let propertyMatch: RegExpExecArray; 31 | while (propertyMatch = cssPropertyPattern.exec(rangeText)) { 32 | const propertyValuePrefix: string = propertyMatch[1]; 33 | const propertyName: string = propertyMatch[2]; 34 | const propertyValue: string = propertyMatch[3]; 35 | 36 | if (!cssDataManager.isKnownProperty(propertyName)) { 37 | continue; 38 | } 39 | 40 | const cssProperty: IPropertyData = cssDataManager.getProperty(propertyName); 41 | if (cssProperty.restrictions && cssProperty.restrictions.includes("color")) { 42 | let colorMatch: RegExpExecArray; 43 | 44 | // RGB hex 45 | while (colorMatch = rgbHexPattern.exec(propertyValue)) { 46 | const rgbHexValue: string = colorMatch[1]; 47 | const colorRange: Range = new Range( 48 | document.positionAt(rangeTextOffset + propertyMatch.index + propertyValuePrefix.length + colorMatch.index), 49 | document.positionAt(rangeTextOffset + propertyMatch.index + propertyValuePrefix.length + colorMatch.index + colorMatch[0].length) 50 | ); 51 | 52 | result.push(new ColorInformation(colorRange, hexToColor(rgbHexValue))); 53 | } 54 | 55 | // RGB function 56 | while (colorMatch = rgbFuncPattern.exec(propertyValue)) { 57 | const r: string = colorMatch[1]; 58 | const g: string = colorMatch[2]; 59 | const b: string = colorMatch[3]; 60 | const a: string = colorMatch[4]; 61 | const colorRange: Range = new Range( 62 | document.positionAt(rangeTextOffset + propertyMatch.index + propertyValuePrefix.length + colorMatch.index), 63 | document.positionAt(rangeTextOffset + propertyMatch.index + propertyValuePrefix.length + colorMatch.index + colorMatch[0].length) 64 | ); 65 | 66 | let red: number = r.includes("%") ? Number.parseFloat(r) / 100 : Number.parseInt(r) / 255; 67 | let green: number = g.includes("%") ? Number.parseInt(g) / 100 : Number.parseFloat(g) / 255; 68 | let blue: number = b.includes("%") ? Number.parseInt(b) / 100 : Number.parseFloat(b) / 255; 69 | let alpha: number; 70 | if (a) { 71 | alpha = a.includes("%") ? Number.parseFloat(a) / 100 : Number.parseFloat(a); 72 | } else { 73 | alpha = 1; 74 | } 75 | 76 | result.push(new ColorInformation(colorRange, new Color(red, green, blue, alpha))); 77 | } 78 | 79 | // HSL function 80 | while (colorMatch = hslFuncPattern.exec(propertyValue)) { 81 | const h: string = colorMatch[1]; 82 | const hUnit: string = colorMatch[2]; 83 | const s: string = colorMatch[3]; 84 | const l: string = colorMatch[4]; 85 | const a: string = colorMatch[5]; 86 | const colorRange: Range = new Range( 87 | document.positionAt(rangeTextOffset + propertyMatch.index + propertyValuePrefix.length + colorMatch.index), 88 | document.positionAt(rangeTextOffset + propertyMatch.index + propertyValuePrefix.length + colorMatch.index + colorMatch[0].length) 89 | ); 90 | 91 | let hue: number = Number.parseFloat(h); 92 | let sat: number = Number.parseFloat(s); 93 | let light: number = Number.parseFloat(l); 94 | let alpha: number; 95 | if (a) { 96 | alpha = a.includes("%") ? Number.parseFloat(a) / 100 : Number.parseFloat(a); 97 | } else { 98 | alpha = 1; 99 | } 100 | const hueUnit = hUnit ? hUnit as "deg" | "rad" | "grad" | "turn" : "deg"; 101 | 102 | result.push(new ColorInformation(colorRange, colorFromHSL({ h: hue, s: sat, l: light, a: alpha }, hueUnit))); 103 | } 104 | 105 | // Color keywords 106 | while (colorMatch = colorKeywordPattern.exec(propertyValue)) { 107 | const keywordPrefix: string = colorMatch[1]; 108 | const colorKeyword: string = colorMatch[2].toLowerCase(); 109 | const colorRange: Range = new Range( 110 | document.positionAt(rangeTextOffset + propertyMatch.index + propertyValuePrefix.length + colorMatch.index + keywordPrefix.length), 111 | document.positionAt(rangeTextOffset + propertyMatch.index + propertyValuePrefix.length + colorMatch.index + keywordPrefix.length + colorKeyword.length) 112 | ); 113 | 114 | result.push(new ColorInformation(colorRange, hexToColor(cssColors[colorKeyword]))); 115 | } 116 | } 117 | } 118 | } 119 | 120 | return result; 121 | } 122 | 123 | /** 124 | * Provide representations for a color. 125 | * @param color The color to show and insert 126 | * @param context A context object with additional information 127 | * @param _token A cancellation token 128 | */ 129 | public async provideColorPresentations(color: Color, context: { document: TextDocument, range: Range }, _token: CancellationToken | boolean): Promise { 130 | let result: ColorPresentation[] = []; 131 | let red256 = Math.round(color.red * 255), green256 = Math.round(color.green * 255), blue256 = Math.round(color.blue * 255); 132 | 133 | let label: string; 134 | if (color.alpha === 1) { 135 | label = `rgb(${red256}, ${green256}, ${blue256})`; 136 | } else { 137 | label = `rgba(${red256}, ${green256}, ${blue256}, ${color.alpha})`; 138 | } 139 | result.push({ label: label, textEdit: TextEdit.replace(context.range, label) }); 140 | 141 | const documentStateContext: DocumentStateContext = getDocumentStateContext(context.document); 142 | const hexPrefix = isInCfOutput(documentStateContext, context.range.start) ? "##" : "#"; 143 | if (color.alpha === 1) { 144 | label = `${hexPrefix}${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}`; 145 | } else { 146 | label = `${hexPrefix}${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}${toTwoDigitHex(Math.round(color.alpha * 255))}`; 147 | } 148 | 149 | result.push({ label: label, textEdit: TextEdit.replace(context.range, label) }); 150 | 151 | const hsl = hslFromColor(color); 152 | if (hsl.a === 1) { 153 | label = `hsl(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%)`; 154 | } else { 155 | label = `hsla(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%, ${hsl.a})`; 156 | } 157 | result.push({ label: label, textEdit: TextEdit.replace(context.range, label) }); 158 | 159 | return result; 160 | } 161 | } 162 | 163 | function toTwoDigitHex(n: number): string { 164 | const r = n.toString(16); 165 | return r.length !== 2 ? "0" + r : r; 166 | } 167 | 168 | function fromTwoDigitHex(hex: string): number { 169 | return Number.parseInt(hex, 16); 170 | } 171 | 172 | function hexToColor(rgbHex: string): Color { 173 | rgbHex = rgbHex.replace(/#/g, ""); 174 | 175 | let red: number; 176 | let green: number; 177 | let blue: number; 178 | let alpha: number; 179 | if (rgbHex.length === 3 || rgbHex.length === 4) { 180 | red = fromTwoDigitHex(rgbHex.substr(0, 1).repeat(2)) / 255; 181 | green = fromTwoDigitHex(rgbHex.substr(1, 1).repeat(2)) / 255; 182 | blue = fromTwoDigitHex(rgbHex.substr(2, 1).repeat(2)) / 255; 183 | alpha = rgbHex.length === 4 ? fromTwoDigitHex(rgbHex.substr(3, 1).repeat(2)) / 255 : 1; 184 | } else if (rgbHex.length === 6 || rgbHex.length === 8) { 185 | red = fromTwoDigitHex(rgbHex.substr(0, 2)) / 255; 186 | green = fromTwoDigitHex(rgbHex.substr(2, 2)) / 255; 187 | blue = fromTwoDigitHex(rgbHex.substr(4, 2)) / 255; 188 | alpha = rgbHex.length === 8 ? fromTwoDigitHex(rgbHex.substr(6, 2)) / 255 : 1; 189 | } else { 190 | return undefined; 191 | } 192 | 193 | return new Color(red, green, blue, alpha); 194 | } 195 | 196 | interface HSLA { h: number; s: number; l: number; a: number; } 197 | 198 | function hslFromColor(rgba: Color): HSLA { 199 | const r = rgba.red; 200 | const g = rgba.green; 201 | const b = rgba.blue; 202 | const a = rgba.alpha; 203 | 204 | const max = Math.max(r, g, b); 205 | const min = Math.min(r, g, b); 206 | let h = 0; 207 | let s = 0; 208 | const l = (min + max) / 2; 209 | const chroma = max - min; 210 | 211 | if (chroma > 0) { 212 | s = Math.min((l <= 0.5 ? chroma / (2 * l) : chroma / (2 - (2 * l))), 1); 213 | 214 | switch (max) { 215 | case r: h = (g - b) / chroma + (g < b ? 6 : 0); break; 216 | case g: h = (b - r) / chroma + 2; break; 217 | case b: h = (r - g) / chroma + 4; break; 218 | } 219 | 220 | h *= 60; 221 | h = Math.round(h); 222 | } 223 | return { h, s, l, a }; 224 | } 225 | 226 | /** 227 | * Converts HSLA values into `Color` 228 | * @param hsla The hue, saturation, lightness, and alpha values. Hue is in units based on `hueUnit`. Saturation and lightness are percentages. 229 | * @param hueUnit One of deg, rad, grad, turn 230 | */ 231 | function colorFromHSL(hsla: HSLA, hueUnit: "deg" | "rad" | "grad" | "turn" = "deg"): Color { 232 | let hue: number; 233 | switch (hueUnit) { 234 | case "deg": hue = hsla.h / 60.0; break; 235 | case "rad": hue = hsla.h * 3 / Math.PI; break; 236 | case "grad": hue = hsla.h * 6 / 400; break; 237 | case "turn": hue = hsla.h * 6; break; 238 | } 239 | const sat = hsla.s / 100; 240 | const light = hsla.l / 100; 241 | 242 | if (sat === 0) { 243 | return new Color(light, light, light, hsla.a); 244 | } else { 245 | const hueToRgb = (t1: number, t2: number, h: number) => { 246 | while (h < 0) { h += 6; } 247 | while (h >= 6) { h -= 6; } 248 | 249 | if (h < 1) { return (t2 - t1) * h + t1; } 250 | if (h < 3) { return t2; } 251 | if (h < 4) { return (t2 - t1) * (4 - h) + t1; } 252 | return t1; 253 | }; 254 | const t2 = light <= 0.5 ? (light * (sat + 1)) : (light + sat - (light * sat)); 255 | const t1 = light * 2 - t2; 256 | return new Color(hueToRgb(t1, t2, hue + 2), hueToRgb(t1, t2, hue), hueToRgb(t1, t2, hue - 2), hsla.a); 257 | } 258 | } 259 | --------------------------------------------------------------------------------