├── test ├── examples │ ├── User.php │ ├── showDollarSign.php │ ├── collapseTypeWhenEqual.php │ ├── collapseHintsWhenEqual.php │ ├── showFullType.php │ ├── general.php │ ├── providers.php │ └── middlewares.php ├── utils.js ├── runTest.js ├── index.js ├── providers.test.js ├── functionGroupsFacade.test.js ├── hints.test.js ├── commands.test.js ├── update.test.js ├── middlewares.test.js └── parameterExtractor.test.js ├── php-parameter-hint.png ├── .vscode ├── extensions.json └── launch.json ├── .vscodeignore ├── jsconfig.json ├── src ├── printer.js ├── utils.js ├── providers │ ├── regex.js │ ├── signature.js │ ├── hover.js │ └── regex.spec.js ├── functionGroupsFacade.js ├── hints.js ├── utils.spec.js ├── pipeline.js ├── pipeline.spec.js ├── update.js ├── cache.js ├── parser.js ├── cache.spec.js ├── middlewares.js ├── extension.js ├── parser.spec.js ├── commands.js └── parameterExtractor.js ├── webpack.config.js ├── .eslintrc.json ├── LICENSE ├── .gitignore ├── CHANGELOG.md ├── README.md └── package.json /test/examples/User.php: -------------------------------------------------------------------------------- 1 | { 4 | return new Promise(resolve => { 5 | setTimeout(resolve, ms); 6 | }); 7 | }; 8 | 9 | const examplesFolderPath = path.join(`${__dirname}/examples/`); 10 | 11 | module.exports = { 12 | sleep, 13 | examplesFolderPath 14 | }; 15 | -------------------------------------------------------------------------------- /test/examples/providers.php: -------------------------------------------------------------------------------- 1 | { 11 | channel.appendLine(`${new Date().toLocaleString()} Error: ${err}`); 12 | }; 13 | 14 | module.exports = { 15 | printError 16 | }; 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const path = require('path'); 4 | 5 | /** @type {import('webpack').Configuration} */ 6 | const config = { 7 | target: 'node', 8 | entry: './src/extension.js', 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: 'extension.js', 12 | libraryTarget: 'commonjs2', 13 | devtoolModuleFilenameTemplate: '../[resource-path]' 14 | }, 15 | devtool: 'source-map', 16 | externals: { 17 | vscode: 'commonjs vscode' 18 | }, 19 | resolve: { 20 | extensions: ['.js'] 21 | } 22 | }; 23 | module.exports = config; 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:chai-friendly/recommended"], 3 | "env": { 4 | "browser": false, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true, 8 | "mocha": true 9 | }, 10 | "parserOptions": { 11 | "ecmaVersion": 2019, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "error", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "warn", 20 | "constructor-super": "warn", 21 | "valid-typeof": "warn", 22 | "no-await-in-loop": "off", 23 | "no-restricted-syntax": "off" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/examples/middlewares.php: -------------------------------------------------------------------------------- 1 | typeof value !== 'undefined'; 10 | 11 | /** 12 | * 13 | * @param {string} code 14 | */ 15 | const removeShebang = code => { 16 | const codeArr = code.split('\n'); 17 | 18 | if (codeArr[0].substr(0, 2) === '#!') { 19 | codeArr[0] = ''; 20 | } 21 | 22 | return codeArr.join('\n'); 23 | }; 24 | 25 | const getCopyFunc = () => { 26 | return process.env.NODE_ENV === 'test' ? copy : copy.default; 27 | }; 28 | 29 | /** 30 | * 31 | * @param {number} time in ms 32 | */ 33 | const pause = (time = 0) => new Promise(resolve => setTimeout(resolve, time)); 34 | 35 | module.exports = { 36 | removeShebang, 37 | sameNamePlaceholder, 38 | isDefined, 39 | getCopyFunc, 40 | pause 41 | }; 42 | -------------------------------------------------------------------------------- /src/providers/regex.js: -------------------------------------------------------------------------------- 1 | // Regex to extract param name/type from function definition 2 | const regExDef = /(?<=\(.*)((\.\.\.)?(&)?\$[a-zA-Z0-9_]+)(?=.*\))/gims; 3 | 4 | // Capture the types as well 5 | const regExDefWithTypes = /(?<=\([^(]*)([^,]*(\.\.\.)?(&)?\$[a-zA-Z0-9_]+)(?=.*\))/gims; 6 | 7 | // Regex to extract param name/type from function doc 8 | const regExDoc = /(?<=@param_ )(?:.*?)((\.\.\.)?(&)?\$[a-zA-Z0-9_]+)/gims; 9 | // Capture the types as well 10 | const regExDocWithTypes = /(?<=@param_ )(([^$])+(\.\.\.)?($)?\$[a-zA-Z0-9_]+)/gims; 11 | 12 | const getDocRegex = showTypes => { 13 | if (showTypes === 'disabled') { 14 | return regExDoc; 15 | } 16 | 17 | return regExDocWithTypes; 18 | }; 19 | 20 | const getDefRegex = showTypes => { 21 | if (showTypes === 'disabled') { 22 | return regExDef; 23 | } 24 | 25 | return regExDefWithTypes; 26 | }; 27 | 28 | module.exports = { 29 | getDocRegex, 30 | getDefRegex 31 | }; 32 | -------------------------------------------------------------------------------- /src/functionGroupsFacade.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const vscode = require('vscode'); 3 | const Parser = require('./parser'); 4 | 5 | class FunctionGroupsFacade { 6 | constructor(cacheService) { 7 | this.cacheService = cacheService; 8 | } 9 | 10 | /** 11 | * @param {string} uri 12 | * @param {string} text 13 | */ 14 | async get(uri, text) { 15 | if (await this.cacheService.isCachedTextValid(uri, text)) { 16 | return this.cacheService.getFunctionGroups(uri); 17 | } 18 | 19 | this.cacheService.deleteFunctionGroups(uri); 20 | const isPhp7 = vscode.workspace.getConfiguration('phpParameterHint').get('php7'); 21 | const parser = new Parser(isPhp7); 22 | parser.parse(text); 23 | const { functionGroups } = parser; 24 | await this.cacheService.setFunctionGroups(uri, text, functionGroups); 25 | return functionGroups; 26 | } 27 | } 28 | 29 | module.exports = { 30 | FunctionGroupsFacade 31 | }; 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension 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": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 14 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 15 | "preLaunchTask": "npm: webpack" 16 | }, 17 | { 18 | "name": "Extension Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--extensionTestsPath=${workspaceFolder}/test/index" 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 robertgr991 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 | -------------------------------------------------------------------------------- /src/hints.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | // eslint-disable-next-line no-unused-vars 3 | const { ThemeColor, workspace, Range } = require('vscode'); 4 | 5 | class Hints { 6 | /** 7 | * 8 | * @param {string} message 9 | * @param {Range} range 10 | */ 11 | static paramHint(message, range) { 12 | const config = workspace.getConfiguration('phpParameterHint'); 13 | 14 | return { 15 | range, 16 | renderOptions: { 17 | before: { 18 | opacity: config.get('opacity'), 19 | color: new ThemeColor('phpParameterHint.hintForeground'), 20 | contentText: message, 21 | backgroundColor: new ThemeColor('phpParameterHint.hintBackground'), 22 | margin: `0px ${config.get('margin') + 1}px 0px ${config.get( 23 | 'margin' 24 | )}px;padding: ${config.get('verticalPadding')}px ${config.get('horizontalPadding')}px;`, 25 | borderRadius: `${config.get('borderRadius')}px`, 26 | fontStyle: config.get('fontStyle'), 27 | fontWeight: `${config.get('fontWeight')};font-size:${config.get('fontSize')}px;` 28 | } 29 | } 30 | }; 31 | } 32 | } 33 | 34 | module.exports = Hints; 35 | -------------------------------------------------------------------------------- /test/runTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const path = require('path'); 3 | const cp = require('child_process'); 4 | const { 5 | runTests, 6 | resolveCliPathFromVSCodeExecutablePath, 7 | downloadAndUnzipVSCode 8 | } = require('vscode-test'); 9 | 10 | async function main() { 11 | try { 12 | // The folder containing the Extension Manifest package.json 13 | // Passed to `--extensionDevelopmentPath` 14 | const extensionDevelopmentPath = path.resolve(__dirname, '../'); 15 | 16 | // The path to the extension test runner script 17 | // Passed to --extensionTestsPath 18 | const extensionTestsPath = path.resolve(__dirname, './index'); 19 | const vscodeExecutablePath = await downloadAndUnzipVSCode('insiders'); 20 | const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath); 21 | 22 | // Use cp.spawn / cp.exec for custom setup 23 | cp.spawnSync(cliPath, ['--install-extension', 'bmewburn.vscode-intelephense-client'], { 24 | encoding: 'utf-8', 25 | stdio: 'inherit' 26 | }); 27 | 28 | // Download VS Code, unzip it and run the integration test 29 | await runTests({ 30 | vscodeExecutablePath, 31 | extensionDevelopmentPath, 32 | extensionTestsPath 33 | }); 34 | } catch (err) { 35 | console.error(err); 36 | console.error('Failed to run tests'); 37 | process.exit(1); 38 | } 39 | } 40 | 41 | main(); 42 | -------------------------------------------------------------------------------- /src/utils.spec.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require('mocha'); 2 | const { expect } = require('chai'); 3 | const { isDefined, removeShebang, getCopyFunc } = require('./utils'); 4 | 5 | describe('isDefined', () => { 6 | it('should return a boolean indicating whether the passed argument is defined', () => { 7 | const hint = 'user:'; 8 | expect(isDefined(undefined)).to.be.false; 9 | expect(isDefined(hint)).to.be.true; 10 | }); 11 | }); 12 | 13 | describe('removeShebang', () => { 14 | it('should remove the shebang if it exists', () => { 15 | const withShebang = { 16 | input: `#!\n { 29 | it('should return the default export only when process.env is not "test"', () => { 30 | expect(() => { 31 | const copy = getCopyFunc(); 32 | const values = [1, 2, 3]; 33 | // @ts-ignore 34 | const clonedValues = copy(values); 35 | expect(values).to.deep.equal(clonedValues); 36 | expect(values).to.not.equal(clonedValues); 37 | }).to.not.throw(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const vscode = require('vscode'); 3 | const path = require('path'); 4 | const Mocha = require('mocha'); 5 | const glob = require('glob'); 6 | const { sleep, examplesFolderPath } = require('./utils'); 7 | 8 | function run() { 9 | process.env.NODE_ENV = 'test'; 10 | // Create the mocha test 11 | const mocha = new Mocha({ 12 | ui: 'tdd', 13 | color: true, 14 | timeout: '600s' 15 | }); 16 | 17 | const testsRoot = path.resolve(__dirname, '..'); 18 | 19 | return new Promise((c, e) => { 20 | glob('test/**/**.test.js', { cwd: testsRoot }, (err, files) => { 21 | if (err) { 22 | e(err); 23 | return; 24 | } 25 | 26 | // Add files to the test suite 27 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 28 | mocha.rootHooks({ 29 | beforeAll: async () => { 30 | const uri = vscode.Uri.file(path.join(`${examplesFolderPath}general.php`)); 31 | const document = await vscode.workspace.openTextDocument(uri); 32 | await vscode.window.showTextDocument(document); 33 | await sleep(4000); // wait for file to be completely functional 34 | } 35 | }); 36 | 37 | try { 38 | // Run the mocha test 39 | mocha.run(failures => { 40 | if (failures > 0) { 41 | e(new Error(`${failures} tests failed.`)); 42 | } else { 43 | c(); 44 | } 45 | }); 46 | } catch (error) { 47 | // eslint-disable-next-line no-console 48 | console.error(error); 49 | e(error); 50 | } 51 | }); 52 | }); 53 | } 54 | 55 | module.exports = { 56 | run 57 | }; 58 | -------------------------------------------------------------------------------- /src/pipeline.js: -------------------------------------------------------------------------------- 1 | const { isDefined } = require('./utils'); 2 | 3 | /** 4 | * Pipeline class used to apply middlewares in a pipe style 5 | */ 6 | class Pipeline { 7 | constructor() { 8 | this.steps = []; 9 | } 10 | 11 | /** 12 | * Each argument can be a function or an array with the step function being 13 | * the first element and the rest of the elements are the additional args that 14 | * will be passed to the function. 15 | * They must be set in the order set by the parameters of the function 16 | * definition, the value being processed by the pipe wil be set as the first 17 | * arg when the function is called. 18 | * 19 | * @param {(Function|(any)[])[]} steps 20 | */ 21 | pipe(...steps) { 22 | steps.forEach(step => { 23 | if (!isDefined(step)) return; 24 | 25 | let finalStep; 26 | 27 | if (Array.isArray(step)) { 28 | finalStep = step; 29 | } else { 30 | finalStep = [step]; 31 | } 32 | 33 | this.steps.push(finalStep); 34 | }); 35 | 36 | return this; 37 | } 38 | 39 | /** 40 | * Clear existing pipes 41 | */ 42 | clear() { 43 | this.steps = []; 44 | return this; 45 | } 46 | 47 | /** 48 | * The value to be processed by the pipeline 49 | * 50 | * @param {any} value 51 | * @param {boolean} clearAfter the pipes after computing the value 52 | */ 53 | async process(value, clearAfter = false) { 54 | let currentValue = value; 55 | for (const [step, ...additionalArgs] of this.steps) { 56 | currentValue = await step(currentValue, ...additionalArgs); 57 | } 58 | 59 | if (clearAfter) { 60 | this.clear(); 61 | } 62 | 63 | return currentValue; 64 | } 65 | } 66 | 67 | module.exports = { 68 | Pipeline 69 | }; 70 | -------------------------------------------------------------------------------- /src/pipeline.spec.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require('mocha'); 2 | const { expect } = require('chai'); 3 | const { Pipeline } = require('./pipeline'); 4 | 5 | const double = x => x * 2; 6 | 7 | describe('Pipeline', () => { 8 | describe('with pipes', () => { 9 | it('should correctly execute all the pushed functions', async () => { 10 | const addAsync = async (x, toAdd) => x + toAdd; 11 | const negateAsync = x => new Promise(resolve => setTimeout(() => resolve(-x), 1)); 12 | const pipeline = new Pipeline(); 13 | const initial = 1; 14 | const expected = -3; 15 | const result = await pipeline.pipe(double, [addAsync, 1], negateAsync).process(initial); 16 | expect(result).to.equal(expected); 17 | }); 18 | }); 19 | 20 | describe('without pipes', () => { 21 | it('should return the initial result when there are no pipes', async () => { 22 | let pipeline = new Pipeline(); 23 | const initial = 1; 24 | const expected = 1; 25 | let result = await pipeline.process(initial); 26 | expect(result).to.equal(expected); 27 | 28 | pipeline = new Pipeline(); 29 | result = await pipeline.pipe(undefined).process(initial); 30 | expect(result).to.equal(expected); 31 | }); 32 | }); 33 | describe('clear pipes', () => { 34 | it('should remove all existing pipes', async () => { 35 | const pipeline = new Pipeline(); 36 | const initial = 1; 37 | let result = await pipeline 38 | .pipe(double) 39 | .clear() 40 | .process(initial); 41 | expect(result).to.equal(initial); 42 | await pipeline.pipe(double).process(initial, true); 43 | result = await pipeline.process(initial); 44 | expect(result).to.equal(initial); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/providers/signature.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const vscode = require('vscode'); 3 | const { printError } = require('../printer'); 4 | const { getDocRegex } = require('./regex'); 5 | 6 | const getArgs = async (editor, line, character, showTypes) => { 7 | let signature; 8 | const signatureHelp = await vscode.commands.executeCommand( 9 | 'vscode.executeSignatureHelpProvider', 10 | editor.document.uri, 11 | new vscode.Position(line, character) 12 | ); 13 | 14 | if (signatureHelp) { 15 | [signature] = signatureHelp.signatures; 16 | } 17 | 18 | if (signature && signature.parameters) { 19 | try { 20 | return signature.parameters.map(parameter => { 21 | const regExDoc = getDocRegex(showTypes); 22 | /** 23 | * If there is a phpDoc for the parameter, use it as the doc 24 | * provides more types 25 | */ 26 | if (parameter.documentation && parameter.documentation.value) { 27 | const docLabel = new RegExp(regExDoc.source, 'gims') 28 | .exec(parameter.documentation.value)[1] 29 | .replace('`', '') 30 | .trim(); 31 | 32 | /** 33 | * Doc wrongfully shows variadic param type as array so we remove it 34 | */ 35 | return docLabel.indexOf('[]') !== -1 && docLabel.indexOf('...') !== -1 36 | ? docLabel.replace('[]', '') 37 | : docLabel; 38 | } 39 | 40 | // Fallback to label 41 | const splittedLabel = parameter.label.split(' '); 42 | 43 | if (showTypes === 'disabled') { 44 | return splittedLabel[0]; 45 | } 46 | 47 | /** 48 | * For cases with default param, like: '$glue = ""', 49 | * take only the param name 50 | */ 51 | return splittedLabel[0].indexOf('$') !== -1 52 | ? splittedLabel[0] 53 | : splittedLabel.slice(0, 2).join(' '); 54 | }); 55 | } catch (err) { 56 | printError(err); 57 | return []; 58 | } 59 | } 60 | 61 | return []; 62 | }; 63 | 64 | module.exports = { 65 | getArgs 66 | }; 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | #VS Code 107 | .vscode-test/ 108 | *.vsix 109 | .DS_STORE 110 | .history/ 111 | .vscode/settings.json 112 | .vscode/snipsnap.code-snippets 113 | -------------------------------------------------------------------------------- /src/providers/hover.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const vscode = require('vscode'); 3 | const { printError } = require('../printer'); 4 | const { getDocRegex, getDefRegex } = require('./regex'); 5 | const { isDefined } = require('../utils'); 6 | 7 | const getArgs = async (editor, line, character, showTypes) => { 8 | let argsDef = []; 9 | let args = []; 10 | const regExDoc = getDocRegex(showTypes); 11 | const regExDef = getDefRegex(showTypes); 12 | 13 | try { 14 | const hoverCommand = await vscode.commands.executeCommand( 15 | 'vscode.executeHoverProvider', 16 | editor.document.uri, 17 | new vscode.Position(line, character) 18 | ); 19 | 20 | if (hoverCommand) { 21 | for (const hover of hoverCommand) { 22 | if (args.length) { 23 | break; 24 | } 25 | 26 | for (const content of hover.contents) { 27 | if (args.length) { 28 | break; 29 | } 30 | 31 | const paramMatches = content.value.match(regExDoc); 32 | 33 | if (Array.isArray(paramMatches) && paramMatches.length) { 34 | args = [ 35 | ...new Set( 36 | paramMatches 37 | .map(label => { 38 | if (!isDefined(label)) { 39 | return label; 40 | } 41 | 42 | if (showTypes === 'disabled' && label.split(' ').length > 1) { 43 | return label 44 | .split(' ')[1] 45 | .replace('`', '') 46 | .trim(); 47 | } 48 | 49 | return label.replace('`', '').trim(); 50 | }) 51 | .filter(label => isDefined(label) && label !== '') 52 | ) 53 | ]; 54 | } 55 | 56 | // If no parameters annotations found, try a regEx that takes the 57 | // parameters from the function definition in hover content 58 | if (!argsDef.length) { 59 | argsDef = [...new Set(content.value.match(regExDef))]; 60 | } 61 | } 62 | } 63 | 64 | if (!args || !args.length) { 65 | args = argsDef; 66 | } 67 | } 68 | } catch (err) { 69 | printError(err); 70 | return []; 71 | } 72 | 73 | return args; 74 | }; 75 | 76 | module.exports = { 77 | getArgs 78 | }; 79 | -------------------------------------------------------------------------------- /src/update.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const vscode = require('vscode'); 3 | // const { singleton } = require('js-coroutines'); 4 | const getHints = require('./parameterExtractor'); 5 | const { printError } = require('./printer'); 6 | const Hints = require('./hints'); 7 | const { pause } = require('./utils'); 8 | 9 | const hintDecorationType = vscode.window.createTextEditorDecorationType({}); 10 | const slowAfterNrParam = 300; 11 | const showParamsOnceEvery = 100; 12 | let runId = 0; 13 | 14 | /** 15 | * The function that creates the new decorations, if the number of arguments 16 | * is bigger than slowAfterNrParam, then the update of the decorations will be 17 | * called once every showParamsOnceEvery 18 | * 19 | * When the function is called, the last call it's interrupted 20 | * 21 | * @param {vscode.TextEditor} activeEditor 22 | * @param {array} functionGroups 23 | */ 24 | async function update(activeEditor, functionGroups) { 25 | runId = Date.now(); 26 | const currentRunId = runId; 27 | const shouldContinue = () => runId === currentRunId; 28 | const argumentsLen = functionGroups.reduce((accumulator, currentGroup) => { 29 | return accumulator + currentGroup.args.length; 30 | }, 0); 31 | let nrArgs = 0; 32 | const phpDecorations = []; 33 | const functionGroupsLen = functionGroups.length; 34 | const functionDictionary = new Map(); 35 | 36 | for (let index = 0; index < functionGroupsLen; index += 1) { 37 | if (!shouldContinue()) { 38 | return null; 39 | } 40 | 41 | const functionGroup = functionGroups[index]; 42 | let hints; 43 | 44 | try { 45 | hints = await getHints(functionDictionary, functionGroup, activeEditor); 46 | } catch (err) { 47 | printError(err); 48 | } 49 | 50 | if (hints && hints.length) { 51 | for (const hint of hints) { 52 | const decorationPHP = Hints.paramHint(hint.text, hint.range); 53 | phpDecorations.push(decorationPHP); 54 | nrArgs += 1; 55 | 56 | if (argumentsLen > slowAfterNrParam) { 57 | if (nrArgs % showParamsOnceEvery === 0) { 58 | activeEditor.setDecorations(hintDecorationType, phpDecorations); 59 | // Continue on next event loop iteration 60 | await pause(10); 61 | 62 | if (!shouldContinue()) { 63 | return null; 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | await pause(10); 72 | 73 | if (!shouldContinue()) { 74 | return null; 75 | } 76 | 77 | activeEditor.setDecorations(hintDecorationType, phpDecorations); 78 | return phpDecorations; 79 | } 80 | 81 | module.exports = { 82 | update 83 | }; 84 | -------------------------------------------------------------------------------- /test/providers.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const vscode = require('vscode'); 3 | const path = require('path'); 4 | const { describe, it, before, after } = require('mocha'); 5 | const { expect } = require('chai'); 6 | const { sleep, examplesFolderPath } = require('./utils'); 7 | const { FunctionGroupsFacade } = require('../src/functionGroupsFacade'); 8 | const { CacheService } = require('../src/cache'); 9 | const signature = require('../src/providers/signature'); 10 | const hover = require('../src/providers/hover'); 11 | 12 | describe('providers', () => { 13 | /** @type {vscode.TextEditor} */ 14 | let editor; 15 | let functionGroups; 16 | 17 | before(async () => { 18 | const functionGroupsFacade = new FunctionGroupsFacade(new CacheService()); 19 | const uri = vscode.Uri.file(path.join(`${examplesFolderPath}providers.php`)); 20 | const document = await vscode.workspace.openTextDocument(uri); 21 | editor = await vscode.window.showTextDocument(document); 22 | await sleep(500); // wait for file to be completely functional 23 | functionGroups = await functionGroupsFacade.get( 24 | editor.document.uri.toString(), 25 | editor.document.getText() 26 | ); 27 | }); 28 | after(async () => { 29 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 30 | }); 31 | 32 | const getArgs = async (provider, showType) => { 33 | /** @type {array} */ 34 | const args = await Promise.all( 35 | functionGroups.map(functionGroup => { 36 | const line = provider === signature ? functionGroup.args[0].start.line : functionGroup.line; 37 | const character = 38 | provider === signature ? functionGroup.args[0].start.character : functionGroup.character; 39 | return provider.getArgs(editor, line, character, showType); 40 | }) 41 | ); 42 | 43 | // @ts-ignore 44 | return args.flat(); 45 | }; 46 | const providers = [ 47 | { 48 | name: 'signature', 49 | func: signature 50 | }, 51 | { 52 | name: 'hover', 53 | func: hover 54 | } 55 | ]; 56 | 57 | for (const provider of providers) { 58 | describe(provider.name, () => { 59 | it('should return only parameters names', async () => { 60 | const args = await getArgs(provider.func, 'disabled'); 61 | const expectedArgs = ['$glue', '$pieces', '$name', '...$ages']; 62 | expect(args).to.deep.equal(expectedArgs); 63 | }); 64 | it('should return parameters names and types', async () => { 65 | let args = await getArgs(provider.func, 'type and name'); 66 | const expectedArgs = ['string $glue', 'array $pieces', 'string $name', 'mixed ...$ages']; 67 | expect(args).to.deep.equal(expectedArgs); 68 | args = await getArgs(provider.func, 'type'); 69 | expect(args).to.deep.equal(expectedArgs); 70 | }); 71 | }); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /src/cache.js: -------------------------------------------------------------------------------- 1 | const LZUTF8 = require('lzutf8'); 2 | const NodeCache = require('node-cache'); 3 | const { isDefined, getCopyFunc } = require('./utils'); 4 | 5 | const copy = getCopyFunc(); 6 | 7 | /** 8 | * Cache service for functions groups per uri 9 | */ 10 | class CacheService { 11 | constructor(cacheTimeInSecs = 60 * 10, checkIntervalInSecs = 60 * 1) { 12 | this.cacheTimeInSecs = cacheTimeInSecs; 13 | this.cache = new NodeCache({ 14 | stdTTL: cacheTimeInSecs, 15 | checkperiod: checkIntervalInSecs, 16 | useClones: false 17 | }); 18 | } 19 | 20 | // Remove all cached data 21 | removeAll() { 22 | this.cache.flushAll(); 23 | } 24 | 25 | /** 26 | * Cache the function groups per uri 27 | * 28 | * @param {string} uri 29 | * @param {string} text 30 | * @param {array} functionGroups 31 | */ 32 | setFunctionGroups(uri, text, functionGroups) { 33 | return new Promise(resolve => { 34 | LZUTF8.compressAsync(text, undefined, (result, error) => { 35 | if (isDefined(error) || !isDefined(result)) { 36 | // Fail silently without adding the data to cache 37 | resolve(); 38 | return; 39 | } 40 | 41 | const data = { 42 | compressedText: result, 43 | // @ts-ignore 44 | functionGroups: copy(functionGroups) 45 | }; 46 | this.cache.set(uri, data); 47 | resolve(); 48 | }); 49 | }); 50 | } 51 | 52 | /** 53 | * 54 | * @param {string} uri 55 | */ 56 | getFunctionGroups(uri) { 57 | const cachedData = this.cache.get(uri); 58 | 59 | if (isDefined(cachedData) && isDefined(cachedData.functionGroups)) { 60 | // If key exists, refresh TTL 61 | this.cache.ttl(uri, this.cacheTimeInSecs); 62 | // @ts-ignore 63 | return copy(cachedData.functionGroups); 64 | } 65 | 66 | return []; 67 | } 68 | 69 | /** 70 | * 71 | * @param {string} uri 72 | */ 73 | deleteFunctionGroups(uri) { 74 | this.cache.del(uri); 75 | } 76 | 77 | /** 78 | * Check if text from uri is the same as the cached text 79 | * @param {string} uri 80 | * @param {string} text 81 | */ 82 | isCachedTextValid(uri, text) { 83 | return new Promise(resolve => { 84 | if (!this.cache.has(uri)) { 85 | resolve(false); 86 | return; 87 | } 88 | 89 | const { compressedText } = this.cache.get(uri); 90 | 91 | if (!isDefined(compressedText)) { 92 | resolve(false); 93 | return; 94 | } 95 | 96 | LZUTF8.decompressAsync(compressedText, undefined, (cachedText, error) => { 97 | if (isDefined(error) || !isDefined(cachedText)) { 98 | resolve(false); 99 | return; 100 | } 101 | 102 | if (text !== cachedText) { 103 | resolve(false); 104 | return; 105 | } 106 | 107 | resolve(true); 108 | }); 109 | }); 110 | } 111 | } 112 | 113 | module.exports = { 114 | CacheService 115 | }; 116 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const engine = require('php-parser'); 3 | const { removeShebang } = require('./utils'); 4 | 5 | class Parser { 6 | /** 7 | * Is php 7.0+ 8 | * @param {boolean} isPhp7 9 | */ 10 | constructor(isPhp7 = true) { 11 | this.functionGroups = []; 12 | // @ts-ignore 13 | // eslint-disable-next-line new-cap 14 | this.parser = new engine({ 15 | parser: { 16 | extractDoc: true, 17 | php7: isPhp7 18 | }, 19 | ast: { 20 | withPositions: true, 21 | withSource: true 22 | }, 23 | lexer: { 24 | short_tags: true, 25 | asp_tags: true, 26 | all_tokens: true, 27 | comment_tokens: true 28 | } 29 | }); 30 | } 31 | 32 | /** 33 | * @param {string} text 34 | */ 35 | parse(text) { 36 | this.functionGroups = []; 37 | const astRoot = this.parser.parseCode(removeShebang(text)); 38 | this.crawl(astRoot); 39 | } 40 | 41 | crawl(ast) { 42 | if (['call', 'new'].includes(ast.kind)) { 43 | try { 44 | this.parseArguments(ast); 45 | // eslint-disable-next-line no-empty 46 | } catch (err) {} 47 | } 48 | 49 | try { 50 | // eslint-disable-next-line no-unused-vars 51 | Object.entries(ast).forEach(([_, node]) => { 52 | if (node instanceof Object) { 53 | try { 54 | this.crawl(node); 55 | // eslint-disable-next-line no-empty 56 | } catch (err) {} 57 | } 58 | }); 59 | // eslint-disable-next-line no-empty 60 | } catch (err) {} 61 | } 62 | 63 | parseArguments(obj) { 64 | const expressionLoc = obj.what.offset ? obj.what.offset.loc.start : obj.what.loc.end; 65 | const functionGroup = { 66 | name: '', 67 | args: [], 68 | line: parseInt(expressionLoc.line, 10) - 1, 69 | character: parseInt(expressionLoc.column, 10) 70 | }; 71 | 72 | if (obj.what && obj.what.kind === 'classreference') { 73 | functionGroup.name = obj.what.name; 74 | } 75 | 76 | obj.arguments.forEach((arg, index) => { 77 | let argument = arg; 78 | 79 | while (argument.kind === 'bin' && argument.left) { 80 | argument = argument.left; 81 | } 82 | 83 | const startLoc = argument.loc.start; 84 | const endLoc = argument.loc.end; 85 | let argKind = argument.kind || ''; 86 | 87 | if ( 88 | argument.kind && 89 | argument.kind === 'identifier' && 90 | argument.name.name && 91 | argument.name.name === 'null' 92 | ) { 93 | argKind = 'null'; 94 | } 95 | 96 | functionGroup.args.push({ 97 | key: index, 98 | start: { 99 | line: parseInt(startLoc.line, 10) - 1, 100 | character: parseInt(startLoc.column, 10) 101 | }, 102 | end: { 103 | line: parseInt(endLoc.line, 10) - 1, 104 | character: parseInt(endLoc.column, 10) 105 | }, 106 | name: argument.name || '', 107 | kind: argKind 108 | }); 109 | }); 110 | 111 | if (functionGroup.args.length && obj.what && obj.what.kind !== 'variable') { 112 | this.functionGroups.push(functionGroup); 113 | } 114 | } 115 | } 116 | 117 | module.exports = Parser; 118 | -------------------------------------------------------------------------------- /src/cache.spec.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require('mocha'); 2 | const { expect } = require('chai'); 3 | const sinon = require('sinon'); 4 | const { CacheService } = require('./cache'); 5 | 6 | describe('CacheService', () => { 7 | describe('set, get and delete', () => { 8 | it('should correctly execute all the operations', async () => { 9 | const cacheService = new CacheService(); 10 | const uri = 'uri'; 11 | const text = 'text'; 12 | const functionGroups = [1, 2, 3]; 13 | await cacheService.setFunctionGroups(uri, text, functionGroups); 14 | let retrievedFunctionGroups = cacheService.getFunctionGroups(uri); 15 | // Function groups are deep copied so it's not the same reference 16 | expect(retrievedFunctionGroups).to.not.equal(functionGroups); 17 | expect(retrievedFunctionGroups).to.deep.equal(functionGroups); 18 | cacheService.deleteFunctionGroups(uri); 19 | retrievedFunctionGroups = cacheService.getFunctionGroups(uri); 20 | expect(retrievedFunctionGroups).to.have.lengthOf(0); 21 | }); 22 | }); 23 | describe('TTL expire', () => { 24 | const clock = sinon.useFakeTimers({ 25 | shouldAdvanceTime: true 26 | }); 27 | after(() => { 28 | clock.restore(); 29 | }); 30 | it('should return empty array if TTL has expired', async () => { 31 | const ttlSeconds = 1; 32 | const expireCheckSeconds = 1; 33 | const cacheService = new CacheService(ttlSeconds, expireCheckSeconds); 34 | const uri = 'uri'; 35 | const text = 'text'; 36 | const functionGroups = [1, 2, 3]; 37 | await cacheService.setFunctionGroups(uri, text, functionGroups); 38 | 39 | // wait for cache to be deleted 40 | clock.tick(3000); // sinon uses milliseconds so 3 seconds 41 | const retrievedFunctionGroups = cacheService.getFunctionGroups(uri); 42 | expect(retrievedFunctionGroups).to.have.lengthOf(0); 43 | }); 44 | }); 45 | describe('check valid cached text', () => { 46 | it('should return true only if the text is the same as the cached text', async () => { 47 | const cacheService = new CacheService(); 48 | const uri = 'uri'; 49 | const text = 'text'; 50 | const functionGroups = [1, 2, 3]; 51 | await cacheService.setFunctionGroups(uri, text, functionGroups); 52 | // Successful retrieval 53 | let isValid = await cacheService.isCachedTextValid(uri, text); 54 | expect(isValid).to.be.true; 55 | // With different text 56 | const newText = 'text2'; 57 | isValid = await cacheService.isCachedTextValid(uri, newText); 58 | expect(isValid).to.be.false; 59 | // After cache is deleted 60 | cacheService.deleteFunctionGroups(uri); 61 | isValid = await cacheService.isCachedTextValid(uri, text); 62 | expect(isValid).to.be.false; 63 | }); 64 | }); 65 | describe('remove all cached data', () => { 66 | it('should return all existing cached data', async () => { 67 | const cacheService = new CacheService(); 68 | const uri1 = 'uri1'; 69 | const text1 = 'text1'; 70 | const uri2 = 'uri2'; 71 | const text2 = 'text2'; 72 | const functionGroups = [1, 2, 3]; 73 | await cacheService.setFunctionGroups(uri1, text1, functionGroups); 74 | await cacheService.setFunctionGroups(uri2, text2, functionGroups); 75 | cacheService.removeAll(); 76 | const retrievedFunctionGroups1 = cacheService.getFunctionGroups(uri1); 77 | expect(retrievedFunctionGroups1).to.have.lengthOf(0); 78 | const retrievedFunctionGroups2 = cacheService.getFunctionGroups(uri2); 79 | expect(retrievedFunctionGroups2).to.have.lengthOf(0); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/middlewares.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const { Position, Range } = require('vscode'); 3 | 4 | /* eslint-disable no-param-reassign */ 5 | const literals = [ 6 | 'boolean', 7 | 'number', 8 | 'string', 9 | 'magic', 10 | 'nowdoc', 11 | 'array', 12 | 'null', 13 | 'encapsed', 14 | 'nullkeyword' 15 | ]; 16 | 17 | // Keep only arguments that are literals 18 | const onlyLiterals = (functionGroups, shouldApply) => { 19 | if (!shouldApply) { 20 | return functionGroups; 21 | } 22 | 23 | return functionGroups.filter(functionGroup => { 24 | functionGroup.args = functionGroup.args.filter(arg => literals.includes(arg.kind)); 25 | 26 | return functionGroup.args.length > 0; 27 | }); 28 | }; 29 | 30 | const isInSelection = currentSelection => argument => { 31 | if ( 32 | argument.start.line > currentSelection.start.line && 33 | argument.end.line < currentSelection.end.line 34 | ) { 35 | return true; 36 | } 37 | if ( 38 | argument.start.line === currentSelection.start.line && 39 | argument.end.line < currentSelection.end.line 40 | ) { 41 | return argument.end.character > currentSelection.start.character; 42 | } 43 | if ( 44 | argument.start.line === currentSelection.start.line && 45 | argument.end.line === currentSelection.end.line 46 | ) { 47 | return ( 48 | argument.start.character >= currentSelection.start.character || 49 | argument.end.character <= currentSelection.end.character 50 | ); 51 | } 52 | if ( 53 | argument.start.line > currentSelection.start.line && 54 | argument.end.line === currentSelection.end.line 55 | ) { 56 | return argument.start.character < currentSelection.end.character; 57 | } 58 | 59 | return false; 60 | }; 61 | 62 | // Keep only arguments in current line/selection 63 | const onlySelection = (functionGroups, activeEditor, shouldApply) => { 64 | if (!shouldApply) { 65 | return functionGroups; 66 | } 67 | 68 | const currentSelection = activeEditor.selection; 69 | let callback; 70 | 71 | if (currentSelection) { 72 | if (currentSelection.isEmpty) { 73 | const lines = []; 74 | 75 | activeEditor.selections.forEach(selection => { 76 | if (selection.isEmpty) { 77 | lines.push(selection.start.line); 78 | } 79 | }); 80 | 81 | callback = argument => lines.includes(argument.start.line); 82 | } else { 83 | callback = isInSelection(currentSelection); 84 | } 85 | 86 | return functionGroups.filter(functionGroup => { 87 | functionGroup.args = functionGroup.args.filter(callback); 88 | 89 | return functionGroup.args.length > 0; 90 | }); 91 | } 92 | 93 | return functionGroups; 94 | }; 95 | 96 | const onlyVisibleRanges = (functionGroups, activeEditor, shouldApply) => { 97 | if (!shouldApply) { 98 | return functionGroups; 99 | } 100 | 101 | return functionGroups.filter(functionGroup => { 102 | functionGroup.args = functionGroup.args.filter(arg => { 103 | const { visibleRanges } = activeEditor; 104 | 105 | for (const range of visibleRanges) { 106 | const argRange = new Range( 107 | new Position(arg.start.line, arg.start.character), 108 | new Position(arg.end.line, arg.end.character) 109 | ); 110 | 111 | if (range.contains(argRange)) { 112 | return true; 113 | } 114 | } 115 | 116 | return false; 117 | }); 118 | 119 | return functionGroup.args.length > 0; 120 | }); 121 | }; 122 | 123 | module.exports = { onlyLiterals, onlySelection, onlyVisibleRanges }; 124 | -------------------------------------------------------------------------------- /test/functionGroupsFacade.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const vscode = require('vscode'); 3 | const path = require('path'); 4 | const { describe, it, before, after } = require('mocha'); 5 | const { expect } = require('chai'); 6 | const { FunctionGroupsFacade } = require('../src/functionGroupsFacade'); 7 | const { CacheService } = require('../src/cache'); 8 | const { sleep, examplesFolderPath } = require('./utils'); 9 | 10 | describe('FunctionGroupsFacade', () => { 11 | /** @type {vscode.TextEditor} */ 12 | let editor; 13 | 14 | before(async () => { 15 | const uri = vscode.Uri.file(path.join(`${examplesFolderPath}general.php`)); 16 | const document = await vscode.workspace.openTextDocument(uri); 17 | editor = await vscode.window.showTextDocument(document); 18 | await sleep(500); // wait for file to be completely functional 19 | }); 20 | after(async () => { 21 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 22 | }); 23 | 24 | describe('get', () => { 25 | it('should return the correct function groups', async () => { 26 | const functionGroupsFacade = new FunctionGroupsFacade(new CacheService()); 27 | const functionGroups = await functionGroupsFacade.get( 28 | editor.document.uri.toString(), 29 | editor.document.getText() 30 | ); 31 | const expectedFunctionGroups = [ 32 | { 33 | name: '', 34 | args: [ 35 | { 36 | key: 0, 37 | start: { 38 | line: 12, 39 | character: 10 40 | }, 41 | end: { 42 | line: 12, 43 | character: 14 44 | }, 45 | name: '', 46 | kind: 'string' 47 | }, 48 | { 49 | key: 1, 50 | start: { 51 | line: 12, 52 | character: 16 53 | }, 54 | end: { 55 | line: 12, 56 | character: 25 57 | }, 58 | name: '', 59 | kind: 'array' 60 | } 61 | ], 62 | line: 12, 63 | character: 9 64 | }, 65 | { 66 | name: '', 67 | args: [ 68 | { 69 | key: 0, 70 | start: { 71 | line: 13, 72 | character: 9 73 | }, 74 | end: { 75 | line: 13, 76 | character: 10 77 | }, 78 | name: '', 79 | kind: 'number' 80 | }, 81 | { 82 | key: 1, 83 | start: { 84 | line: 13, 85 | character: 12 86 | }, 87 | end: { 88 | line: 13, 89 | character: 13 90 | }, 91 | name: '', 92 | kind: 'number' 93 | }, 94 | { 95 | key: 2, 96 | start: { 97 | line: 13, 98 | character: 15 99 | }, 100 | end: { 101 | line: 13, 102 | character: 16 103 | }, 104 | name: '', 105 | kind: 'number' 106 | } 107 | ], 108 | line: 13, 109 | character: 8 110 | } 111 | ]; 112 | 113 | expect(functionGroups).to.have.lengthOf(2); 114 | expect(functionGroups).to.deep.equal(expectedFunctionGroups); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/hints.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const vscode = require('vscode'); 3 | const path = require('path'); 4 | const { describe, it, before, after } = require('mocha'); 5 | const { expect } = require('chai'); 6 | const sinon = require('sinon'); 7 | const { sleep, examplesFolderPath } = require('./utils'); 8 | const { FunctionGroupsFacade } = require('../src/functionGroupsFacade'); 9 | const { CacheService } = require('../src/cache'); 10 | const getHints = require('../src/parameterExtractor'); 11 | const Hints = require('../src/hints'); 12 | 13 | describe('hints', () => { 14 | /** @type {{text: string, range: vscode.Range}} */ 15 | let hint; 16 | 17 | before(async () => { 18 | const uri = vscode.Uri.file(path.join(`${examplesFolderPath}general.php`)); 19 | const document = await vscode.workspace.openTextDocument(uri); 20 | const editor = await vscode.window.showTextDocument(document); 21 | await sleep(500); // wait for file to be completely functional 22 | const [functionGroup] = await new FunctionGroupsFacade(new CacheService()).get( 23 | editor.document.uri.toString(), 24 | editor.document.getText() 25 | ); 26 | [hint] = await getHints(new Map(), functionGroup, editor); 27 | }); 28 | after(async () => { 29 | sinon.restore(); 30 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 31 | }); 32 | 33 | it('should have the correct range', () => { 34 | const { range } = Hints.paramHint(hint.text, hint.range); 35 | expect(range.start.line).to.equal(hint.range.start.line); 36 | expect(range.start.character).to.equal(hint.range.start.character); 37 | expect(range.end.line).to.equal(hint.range.end.line); 38 | expect(range.end.character).to.equal(hint.range.end.character); 39 | }); 40 | it('should have the correct css props', () => { 41 | const stub = sinon.stub(vscode.workspace, 'getConfiguration'); 42 | const getStub = sinon.stub(); 43 | stub.withArgs('phpParameterHint').returns({ 44 | get: getStub, 45 | has: sinon.fake(), 46 | inspect: sinon.fake(), 47 | update: sinon.fake() 48 | }); 49 | const expectedOpacity = 0.5; 50 | const expectedFontStyle = 'bold'; 51 | const expectedFontWeight = 500; 52 | const expectedFontSize = 13; 53 | const expectedBorderRadius = 6; 54 | const expectedVerticalPadding = 2; 55 | const expectedHorizontalPadding = 5; 56 | const expectedMargin = 3; 57 | const expectedColor = { 58 | id: 'phpParameterHint.hintForeground' 59 | }; 60 | const expectedBackgroundColor = { 61 | id: 'phpParameterHint.hintBackground' 62 | }; 63 | 64 | getStub 65 | .withArgs('opacity') 66 | .returns(expectedOpacity) 67 | .withArgs('fontStyle') 68 | .returns(expectedFontStyle) 69 | .withArgs('fontWeight') 70 | .returns(expectedFontWeight) 71 | .withArgs('fontSize') 72 | .returns(expectedFontSize) 73 | .withArgs('borderRadius') 74 | .returns(expectedBorderRadius) 75 | .withArgs('verticalPadding') 76 | .returns(expectedVerticalPadding) 77 | .withArgs('horizontalPadding') 78 | .returns(expectedHorizontalPadding) 79 | .withArgs('margin') 80 | .returns(expectedMargin); 81 | 82 | const { 83 | renderOptions: { 84 | before: { 85 | opacity, 86 | contentText, 87 | color, 88 | backgroundColor, 89 | margin, 90 | fontStyle, 91 | fontWeight, 92 | borderRadius 93 | } 94 | } 95 | } = Hints.paramHint(hint.text, hint.range); 96 | expect(opacity).to.equal(expectedOpacity); 97 | expect(color).to.deep.equal(expectedColor); 98 | expect(backgroundColor).to.deep.equal(expectedBackgroundColor); 99 | expect(fontStyle).to.equal(expectedFontStyle); 100 | expect(fontWeight).to.equal(`${expectedFontWeight};font-size:${expectedFontSize}px;`); 101 | expect(contentText).to.equal(hint.text); 102 | expect(borderRadius).to.equal(`${expectedBorderRadius}px`); 103 | expect(opacity).to.equal(expectedOpacity); 104 | expect(margin).to.equal( 105 | `0px ${expectedMargin + 106 | 1}px 0px ${expectedMargin}px;padding: ${expectedVerticalPadding}px ${expectedHorizontalPadding}px;` 107 | ); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/commands.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const vscode = require('vscode'); 3 | const path = require('path'); 4 | const { describe, it, before, after } = require('mocha'); 5 | const { expect } = require('chai'); 6 | const { sleep, examplesFolderPath } = require('./utils'); 7 | 8 | describe('commands', () => { 9 | before(async () => { 10 | const uri = vscode.Uri.file(path.join(`${examplesFolderPath}general.php`)); 11 | const document = await vscode.workspace.openTextDocument(uri); 12 | await vscode.window.showTextDocument(document); 13 | await sleep(500); // wait for file to be completely functional 14 | }); 15 | after(async () => { 16 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 17 | }); 18 | 19 | describe('toggleTypeName', () => { 20 | after(async () => { 21 | await vscode.workspace.getConfiguration('phpParameterHint').update('hintTypeName', 0, true); 22 | await sleep(1000); 23 | }); 24 | it('should cycle between available options', async () => { 25 | await vscode.workspace.getConfiguration('phpParameterHint').update('hintTypeName', 0, true); 26 | await sleep(1000); 27 | let hintTypeName = vscode.workspace.getConfiguration('phpParameterHint').get('hintTypeName'); 28 | expect(hintTypeName).to.equal(0); 29 | await vscode.commands.executeCommand('phpParameterHint.toggleTypeName'); 30 | await sleep(1000); 31 | hintTypeName = vscode.workspace.getConfiguration('phpParameterHint').get('hintTypeName'); 32 | expect(hintTypeName).to.equal(1); 33 | await vscode.commands.executeCommand('phpParameterHint.toggleTypeName'); 34 | await sleep(1000); 35 | hintTypeName = vscode.workspace.getConfiguration('phpParameterHint').get('hintTypeName'); 36 | expect(hintTypeName).to.equal(2); 37 | await vscode.commands.executeCommand('phpParameterHint.toggleTypeName'); 38 | await sleep(1000); 39 | hintTypeName = vscode.workspace.getConfiguration('phpParameterHint').get('hintTypeName'); 40 | expect(hintTypeName).to.equal(0); 41 | }); 42 | }); 43 | describe('switchable commands', async () => { 44 | const switchableCommandsTable = [ 45 | { 46 | name: 'toggle', 47 | valueName: 'enabled', 48 | default: true 49 | }, 50 | { 51 | name: 'toggleOnChange', 52 | valueName: 'onChange', 53 | default: false 54 | }, 55 | { 56 | name: 'toggleOnSave', 57 | valueName: 'onSave', 58 | default: true 59 | }, 60 | { 61 | name: 'toggleLiterals', 62 | valueName: 'hintOnlyLiterals', 63 | default: false 64 | }, 65 | { 66 | name: 'toggleLine', 67 | valueName: 'hintOnlyLine', 68 | default: false 69 | }, 70 | { 71 | name: 'toggleVisibleRanges', 72 | valueName: 'hintOnlyVisibleRanges', 73 | default: false 74 | }, 75 | { 76 | name: 'toggleCollapse', 77 | valueName: 'collapseHintsWhenEqual', 78 | default: false 79 | }, 80 | { 81 | name: 'toggleCollapseType', 82 | valueName: 'collapseTypeWhenEqual', 83 | default: false 84 | }, 85 | { 86 | name: 'toggleFullType', 87 | valueName: 'showFullType', 88 | default: false 89 | }, 90 | { 91 | name: 'toggleDollarSign', 92 | valueName: 'showDollarSign', 93 | default: false 94 | } 95 | ]; 96 | after(async () => { 97 | for (const command of switchableCommandsTable) { 98 | await vscode.workspace 99 | .getConfiguration('phpParameterHint') 100 | .update(command.valueName, command.default, true); 101 | } 102 | await sleep(1000); 103 | }); 104 | 105 | for (const command of switchableCommandsTable) { 106 | describe(command.name, () => { 107 | it(`should enable/disable ${command.valueName}`, async () => { 108 | await vscode.workspace 109 | .getConfiguration('phpParameterHint') 110 | .update(command.valueName, true, true); 111 | await sleep(1000); 112 | let value = vscode.workspace.getConfiguration('phpParameterHint').get(command.valueName); 113 | expect(value).to.equal(true); 114 | await vscode.commands.executeCommand(`phpParameterHint.${command.name}`); 115 | await sleep(1000); 116 | value = vscode.workspace.getConfiguration('phpParameterHint').get(command.valueName); 117 | expect(value).to.equal(false); 118 | await vscode.commands.executeCommand(`phpParameterHint.${command.name}`); 119 | await sleep(1000); 120 | value = vscode.workspace.getConfiguration('phpParameterHint').get(command.valueName); 121 | expect(value).to.equal(true); 122 | }); 123 | }); 124 | } 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/update.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const vscode = require('vscode'); 3 | const path = require('path'); 4 | const { describe, it, before, after } = require('mocha'); 5 | const { expect } = require('chai'); 6 | const { sleep, examplesFolderPath } = require('./utils'); 7 | const { FunctionGroupsFacade } = require('../src/functionGroupsFacade'); 8 | const { CacheService } = require('../src/cache'); 9 | const { update } = require('../src/update'); 10 | 11 | describe('update', () => { 12 | /** @type {vscode.TextEditor} */ 13 | let editor; 14 | let functionGroups; 15 | const expectedDecorations = [ 16 | { 17 | range: new vscode.Range(new vscode.Position(12, 10), new vscode.Position(12, 14)), 18 | renderOptions: { 19 | before: { 20 | opacity: 0.4, 21 | color: { 22 | id: 'phpParameterHint.hintForeground' 23 | }, 24 | contentText: 'glue:', 25 | backgroundColor: { 26 | id: 'phpParameterHint.hintBackground' 27 | }, 28 | margin: '0px 3px 0px 2px;padding: 1px 4px;', 29 | borderRadius: '5px', 30 | fontStyle: 'italic', 31 | fontWeight: '400;font-size:12px;' 32 | } 33 | } 34 | }, 35 | { 36 | range: new vscode.Range(new vscode.Position(12, 16), new vscode.Position(12, 25)), 37 | renderOptions: { 38 | before: { 39 | opacity: 0.4, 40 | color: { 41 | id: 'phpParameterHint.hintForeground' 42 | }, 43 | contentText: 'pieces:', 44 | backgroundColor: { 45 | id: 'phpParameterHint.hintBackground' 46 | }, 47 | margin: '0px 3px 0px 2px;padding: 1px 4px;', 48 | borderRadius: '5px', 49 | fontStyle: 'italic', 50 | fontWeight: '400;font-size:12px;' 51 | } 52 | } 53 | }, 54 | { 55 | range: new vscode.Range(new vscode.Position(13, 9), new vscode.Position(13, 10)), 56 | renderOptions: { 57 | before: { 58 | opacity: 0.4, 59 | color: { 60 | id: 'phpParameterHint.hintForeground' 61 | }, 62 | contentText: 'vars[0]:', 63 | backgroundColor: { 64 | id: 'phpParameterHint.hintBackground' 65 | }, 66 | margin: '0px 3px 0px 2px;padding: 1px 4px;', 67 | borderRadius: '5px', 68 | fontStyle: 'italic', 69 | fontWeight: '400;font-size:12px;' 70 | } 71 | } 72 | }, 73 | { 74 | range: new vscode.Range(new vscode.Position(13, 12), new vscode.Position(13, 13)), 75 | renderOptions: { 76 | before: { 77 | opacity: 0.4, 78 | color: { 79 | id: 'phpParameterHint.hintForeground' 80 | }, 81 | contentText: 'vars[1]:', 82 | backgroundColor: { 83 | id: 'phpParameterHint.hintBackground' 84 | }, 85 | margin: '0px 3px 0px 2px;padding: 1px 4px;', 86 | borderRadius: '5px', 87 | fontStyle: 'italic', 88 | fontWeight: '400;font-size:12px;' 89 | } 90 | } 91 | }, 92 | { 93 | range: new vscode.Range(new vscode.Position(13, 15), new vscode.Position(13, 16)), 94 | renderOptions: { 95 | before: { 96 | opacity: 0.4, 97 | color: { 98 | id: 'phpParameterHint.hintForeground' 99 | }, 100 | contentText: 'vars[2]:', 101 | backgroundColor: { 102 | id: 'phpParameterHint.hintBackground' 103 | }, 104 | margin: '0px 3px 0px 2px;padding: 1px 4px;', 105 | borderRadius: '5px', 106 | fontStyle: 'italic', 107 | fontWeight: '400;font-size:12px;' 108 | } 109 | } 110 | } 111 | ]; 112 | 113 | before(async () => { 114 | const uri = vscode.Uri.file(path.join(`${examplesFolderPath}general.php`)); 115 | const document = await vscode.workspace.openTextDocument(uri); 116 | editor = await vscode.window.showTextDocument(document); 117 | await sleep(500); // wait for file to be completely functional 118 | functionGroups = await new FunctionGroupsFacade(new CacheService()).get( 119 | editor.document.uri.toString(), 120 | editor.document.getText() 121 | ); 122 | }); 123 | after(async () => { 124 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 125 | }); 126 | 127 | it('should return the correct decorations', async () => { 128 | const decorations = await update(editor, functionGroups); 129 | expect(decorations).to.deep.equal(expectedDecorations); 130 | }); 131 | it('should cancel the first call and return null when a second one is made and the first one has not finished', async () => { 132 | const [firstDecorations, secondDecorations] = await Promise.all([ 133 | update(editor, functionGroups), 134 | update(editor, functionGroups) 135 | ]); 136 | expect(firstDecorations).to.be.null; 137 | expect(secondDecorations).to.deep.equal(expectedDecorations); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.5.4] - 31-03-2021 4 | 5 | ### Changed 6 | 7 | - Update deps to latest 8 | 9 | ## [0.5.3] - 31-03-2021 10 | 11 | ### Changed 12 | 13 | - Update deps because of security vulnerabilities 14 | 15 | ## [0.5.2] - 31-03-2021 16 | 17 | ### Changed 18 | 19 | - Update deps 20 | 21 | ## [0.5.1] - 17-09-2020 22 | 23 | ### Changed 24 | 25 | - Stop using [js-coroutines](https://github.com/miketalbot/js-coroutines) for 26 | now because it causes high CPU usage 27 | 28 | ## [0.5.0] - 22-08-2020 29 | 30 | ### Added 31 | 32 | - Commands to toggle hinting on text change/document save 33 | - Command to toggle showing `$` before parameter name 34 | - Integration tests in vscode environment 35 | - More unit tests 36 | - `FunctionGroupsFacade` 37 | 38 | ### Fixed 39 | 40 | - RegExp for getting parameter names and types from function definition 41 | - Hover provider when using the documentation RegExp 42 | - Parser when there are only short opening tags or when there is only PHP 43 | embedded in HTML 44 | - Bug when show only visible ranges was forced only locally before `update` it's called 45 | 46 | ### Changed 47 | 48 | - `update` returns the decorations, for testing purposes 49 | 50 | ### Removed 51 | 52 | - Remove margin for visible ranges middleware 53 | 54 | ## [0.4.1] - 13-08-2020 55 | 56 | ### Fixed 57 | 58 | - Add missing `resolve` when there is an error while compressing the text. 59 | 60 | ## [0.4.0] - 13-08-2020 61 | 62 | ### Added 63 | 64 | - Hint only visible ranges middleware, can be toggle with command. Defaults to 65 | `false`. For files with a lot of arguments, it's forcefully enabled. 66 | - In-memory cache for function groups. If document text is different than the cached text, parse the 67 | file again, else return the cached function groups. Cached function groups 68 | have a TTL of 10 min, refreshed with each successful retrieval. 69 | 70 | ### Changed 71 | 72 | - Add unit test for `clear` method of `Pipeline` class. 73 | 74 | ## 0.3.4(10-08-2020) 75 | 76 | - Fix middlewares and pipeline 77 | 78 | ## 0.3.3(10-08-2020) 79 | 80 | - - Set border-radius of hints as configurable 81 | - Set opacity of hints as configurable 82 | - Move construction of final decoration text to `parameterExtractor.js` 83 | - For variadic arguments, when showing only the type, don't show a space 84 | between the type and index - show `mixed[0]` instead of `mixed [0]` 85 | 86 | ## 0.3.2(09-08-2020) 87 | 88 | - Bundle extension 89 | 90 | ## 0.3.1(09-08-2020) 91 | 92 | - Exclude unnecessary files 93 | 94 | ## 0.3.0(09-08-2020) 95 | 96 | - - Add command to collapse type and name when equal 97 | - Add command to show full name of type(namespaces including) 98 | - Use [js-coroutines](https://github.com/miketalbot/js-coroutines) to avoid 99 | high CPU load 100 | - Update php-parser version 101 | - Simplify AST crawling 102 | - Refactoring 103 | 104 | ## 0.2.3(29-07-2020) 105 | 106 | - When available, use doc from signature help provider + some refactoring 107 | 108 | ## 0.2.2(28-07-2020) 109 | 110 | - Fix regex for PHPDoc parsing when using hover provider: https://github.com/DominicVonk 111 | 112 | ## 0.2.1(28-07-2020) 113 | 114 | - Show only the "short" name of the type, without the namespace 115 | 116 | ## 0.2.0(26-07-2020) 117 | 118 | - Add option to toggle between showing only parameter name, type and name or 119 | only type, if type is available 120 | 121 | ## 0.1.5(17-05-2020) 122 | 123 | - Remove the white spaces before and after the hint, to allow it to be fully 124 | customizable by changing the horizontal padding. Increase the default 125 | horizontal padding so decorations look like before the removal of white spaces. 126 | 127 | ## 0.1.4(15-05-2020) 128 | 129 | - Add configuration for the vertical and horizontal padding of the decoration. 130 | 131 | ## 0.1.3(11-04-2020) 132 | 133 | - New way of getting only the text containing php, old function was breaking if 134 | a php tag was in a comment/string. Refactoring. 135 | 136 | ## 0.1.2 (16-03-2020) 137 | 138 | - Update README and fixed bug with hint only current line. 139 | 140 | ## 0.1.1 (15-03-2020) 141 | 142 | - Update parser. Set status bar message after execution of a command. 143 | Separation. Grouping arguments by function, to avoid unnecessary calls to signature helper. 144 | 145 | ## 0.1.0 (15-03-2020) 146 | 147 | - Now parameters are taken from SignatureHelpProvider and the Hover content is 148 | the fallback. 149 | 150 | ## 0.0.9 (14-03-2020) 151 | 152 | - Added unique id to every update call, to cancel previous update call in case a 153 | call is still active when a new update call is made. 154 | 155 | ## 0.0.8 (14-03-2020) 156 | 157 | - Option to show only arguments on current line now shows arguments in 158 | selection. If there is a large number of arguments to display, they will show 159 | once every an arbitrary number. 160 | 161 | ## 0.0.7 (13-03-2020) 162 | 163 | - Update dev dependency "minimist" version 164 | 165 | ## 0.0.6 (13-03-2020) 166 | 167 | - Added option to collapse hints when parameter name matches the variable name, option to show hints only for literals, option to show hints only for current line and changed the regex that extracts the parameters from hover content. 168 | 169 | ## 0.0.5 (12-03-2020) 170 | 171 | - Add support for "@", "break/continue", "eval" and showing the ampersand for parameters passed by reference. 172 | 173 | ## 0.0.1 - 0.0.4 (12-03-2020) 174 | 175 | - Initial release and fixes. 176 | -------------------------------------------------------------------------------- /src/providers/regex.spec.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require('mocha'); 2 | const { expect } = require('chai'); 3 | const { getDocRegex, getDefRegex } = require('./regex'); 4 | 5 | describe('regexp for documentation', () => { 6 | describe('with data types included', () => { 7 | const regExDocTypes = getDocRegex('types'); 8 | 9 | it('should extract the type and name', () => { 10 | const extractedTypeAndNameArr = new RegExp(regExDocTypes.source, 'gims').exec( 11 | '@param_ `\\Models\\User $user`' 12 | ); 13 | expect(extractedTypeAndNameArr).to.have.lengthOf(5); 14 | expect(extractedTypeAndNameArr[1]).to.equal('`\\Models\\User $user'); 15 | }); 16 | it('should return the correct representation when the argument is variadic', () => { 17 | const extractedTypeAndNameArr = new RegExp(regExDocTypes.source, 'gims').exec( 18 | '@param_ `int ...$numbers`' 19 | ); 20 | expect(extractedTypeAndNameArr).to.have.lengthOf(5); 21 | expect(extractedTypeAndNameArr[1]).to.equal('`int ...$numbers'); 22 | }); 23 | it('should extract the correct representation when the argument is passed by reference', () => { 24 | const extractedTypeAndNameArr = new RegExp(regExDocTypes.source, 'gims').exec( 25 | '@param_ `string &$glue`' 26 | ); 27 | expect(extractedTypeAndNameArr).to.have.lengthOf(5); 28 | expect(extractedTypeAndNameArr[1]).to.equal('`string &$glue'); 29 | }); 30 | it('should correctly extract all the params when there are multiple', () => { 31 | const extractedTypeAndNameMatchArr = '@param_ `string $glue` \n @param_ `int ...$numbers`'.match( 32 | new RegExp(regExDocTypes.source, 'gims') 33 | ); 34 | expect(extractedTypeAndNameMatchArr).to.have.lengthOf(2); 35 | expect(extractedTypeAndNameMatchArr[0]).to.equal('`string $glue'); 36 | expect(extractedTypeAndNameMatchArr[1]).to.equal('`int ...$numbers'); 37 | }); 38 | }); 39 | describe('without data types', () => { 40 | const regExDoc = getDocRegex('disabled'); 41 | 42 | it('should extract only the parameter name', () => { 43 | const extractedNameArr = new RegExp(regExDoc.source, 'gims').exec('@param_ `string $glue`'); 44 | expect(extractedNameArr).to.have.lengthOf(4); 45 | expect(extractedNameArr[1]).to.equal('$glue'); 46 | }); 47 | it('should extract the correct representation when the argument is variadic', () => { 48 | const extractedNameArr = new RegExp(regExDoc.source, 'gims').exec( 49 | '@param_ `int ...$numbers`' 50 | ); 51 | expect(extractedNameArr).to.have.lengthOf(4); 52 | expect(extractedNameArr[1]).to.equal('...$numbers'); 53 | }); 54 | it('should extract the correct representation when the argument is passed by reference', () => { 55 | const extractedNameArr = new RegExp(regExDoc.source, 'gims').exec('@param_ `User &$user`'); 56 | expect(extractedNameArr).to.have.lengthOf(4); 57 | expect(extractedNameArr[1]).to.equal('&$user'); 58 | }); 59 | it('should correctly extract all the params when there are multiple', () => { 60 | const extractedNameMatchArr = '@param_ `string $glue` \n @param_ `int ...$numbers`'.match( 61 | new RegExp(regExDoc.source, 'gims') 62 | ); 63 | expect(extractedNameMatchArr).to.have.lengthOf(2); 64 | expect(extractedNameMatchArr[0]).to.equal('`string $glue'); 65 | expect(extractedNameMatchArr[1]).to.equal('`int ...$numbers'); 66 | }); 67 | }); 68 | }); 69 | 70 | describe('regexp for function definition', () => { 71 | describe('with data types included', () => { 72 | const regExDefTypes = getDefRegex('types'); 73 | 74 | it('should extract the type and name', () => { 75 | const extractedTypeAndNameArr = 'function join(string $glue = "", array $pieces)'.match( 76 | new RegExp(regExDefTypes.source, 'gims') 77 | ); 78 | expect(extractedTypeAndNameArr).to.have.lengthOf(2); 79 | expect(extractedTypeAndNameArr[0]).to.equal('string $glue'); 80 | expect(extractedTypeAndNameArr[1]).to.equal(' array $pieces'); 81 | }); 82 | it('should return the correct representation when the argument is variadic', () => { 83 | const extractedTypeAndNameArr = 'function join(int ...$numbers)'.match( 84 | new RegExp(regExDefTypes.source, 'gims') 85 | ); 86 | expect(extractedTypeAndNameArr).to.have.lengthOf(1); 87 | expect(extractedTypeAndNameArr[0]).to.equal('int ...$numbers'); 88 | }); 89 | it('should extract the correct representation when the argument is passed by reference and when there are multiple parameters', () => { 90 | const extractedTypeAndNameArr = 'function join(string &$glue = "", array &$pieces)'.match( 91 | new RegExp(regExDefTypes.source, 'gims') 92 | ); 93 | expect(extractedTypeAndNameArr).to.have.lengthOf(2); 94 | expect(extractedTypeAndNameArr[0]).to.equal('string &$glue'); 95 | expect(extractedTypeAndNameArr[1]).to.equal(' array &$pieces'); 96 | }); 97 | }); 98 | 99 | describe('without data types', () => { 100 | const regExDef = getDefRegex('disabled'); 101 | 102 | it('should extract only the parameter name', () => { 103 | const extractedNameArr = 'function join($glue = "", $pieces)'.match( 104 | new RegExp(regExDef.source, 'gims') 105 | ); 106 | expect(extractedNameArr).to.have.lengthOf(2); 107 | expect(extractedNameArr[0]).to.equal('$glue'); 108 | expect(extractedNameArr[1]).to.equal('$pieces'); 109 | }); 110 | it('should extract the correct representation when the argument is variadic', () => { 111 | const extractedNameArr = 'function join(...$numbers)'.match( 112 | new RegExp(regExDef.source, 'gims') 113 | ); 114 | expect(extractedNameArr).to.have.lengthOf(1); 115 | expect(extractedNameArr[0]).to.equal('...$numbers'); 116 | }); 117 | it('should extract the correct representation when the argument is passed by reference', () => { 118 | const extractedNameArr = 'function join(&$glue)'.match(new RegExp(regExDef.source, 'gims')); 119 | expect(extractedNameArr).to.have.lengthOf(1); 120 | expect(extractedNameArr[0]).to.equal('&$glue'); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /src/extension.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | const vscode = require('vscode'); 3 | const debounce = require('lodash.debounce'); 4 | const { Commands } = require('./commands'); 5 | const { printError } = require('./printer'); 6 | const { update } = require('./update'); 7 | const { onlyLiterals, onlySelection, onlyVisibleRanges } = require('./middlewares'); 8 | const { Pipeline } = require('./pipeline'); 9 | const { CacheService } = require('./cache'); 10 | const { FunctionGroupsFacade } = require('./functionGroupsFacade'); 11 | 12 | const hintDecorationType = vscode.window.createTextEditorDecorationType({}); 13 | const initialNrTries = 3; 14 | 15 | /** 16 | * This method is called when VSCode is activated 17 | * @param {vscode.ExtensionContext} context 18 | */ 19 | function activate(context) { 20 | let timeout; 21 | let activeEditor = vscode.window.activeTextEditor; 22 | const functionGroupsFacade = new FunctionGroupsFacade(new CacheService()); 23 | 24 | /** 25 | * Get the PHP code then parse it and create parameter hints 26 | */ 27 | async function updateDecorations() { 28 | timeout = undefined; 29 | 30 | if (!activeEditor || !activeEditor.document || activeEditor.document.languageId !== 'php') { 31 | return; 32 | } 33 | 34 | const { document: currentDocument } = activeEditor; 35 | const uriStr = currentDocument.uri.toString(); 36 | const isEnabled = vscode.workspace.getConfiguration('phpParameterHint').get('enabled'); 37 | 38 | if (!isEnabled) { 39 | activeEditor.setDecorations(hintDecorationType, []); 40 | 41 | return; 42 | } 43 | 44 | const text = currentDocument.getText(); 45 | let functionGroups = []; 46 | const hintOnChange = vscode.workspace.getConfiguration('phpParameterHint').get('onChange'); 47 | const hintOnlyLine = vscode.workspace.getConfiguration('phpParameterHint').get('hintOnlyLine'); 48 | const hintOnlyLiterals = vscode.workspace 49 | .getConfiguration('phpParameterHint') 50 | .get('hintOnlyLiterals'); 51 | const hintOnlyVisibleRanges = vscode.workspace 52 | .getConfiguration('phpParameterHint') 53 | .get('hintOnlyVisibleRanges'); 54 | 55 | try { 56 | functionGroups = await functionGroupsFacade.get(uriStr, text); 57 | } catch (err) { 58 | printError(err); 59 | 60 | if (hintOnChange || hintOnlyLine) { 61 | return; 62 | } 63 | } 64 | 65 | if (!functionGroups.length) { 66 | activeEditor.setDecorations(hintDecorationType, []); 67 | 68 | return; 69 | } 70 | 71 | const finalFunctionGroups = await new Pipeline() 72 | .pipe( 73 | [onlyLiterals, hintOnlyLiterals], 74 | [onlyVisibleRanges, activeEditor, hintOnlyVisibleRanges], 75 | [onlySelection, activeEditor, hintOnlyLine] 76 | ) 77 | .process(functionGroups); 78 | await update(activeEditor, finalFunctionGroups); 79 | } 80 | 81 | /** 82 | * Trigger updating decorations 83 | * 84 | * @param {number} delay integer 85 | */ 86 | function triggerUpdateDecorations(delay = 1000) { 87 | if (timeout) { 88 | clearTimeout(timeout); 89 | timeout = undefined; 90 | } 91 | 92 | timeout = setTimeout(updateDecorations, delay); 93 | } 94 | 95 | /** 96 | * Try creating hints multiple time on activation, in case intelephense 97 | * extension was not loaded at first 98 | * 99 | * @param {number} numberTries integer 100 | */ 101 | function tryInitial(numberTries) { 102 | if (!numberTries) { 103 | setTimeout(triggerUpdateDecorations, 4000); 104 | 105 | return; 106 | } 107 | 108 | const intelephenseExtension = vscode.extensions.getExtension( 109 | 'bmewburn.vscode-intelephense-client' 110 | ); 111 | 112 | if (!intelephenseExtension || !intelephenseExtension.isActive) { 113 | setTimeout(() => tryInitial(numberTries - 1), 2000); 114 | } else { 115 | setTimeout(triggerUpdateDecorations, 4000); 116 | } 117 | } 118 | 119 | vscode.workspace.onDidChangeConfiguration(event => { 120 | if (event.affectsConfiguration('phpParameterHint')) { 121 | triggerUpdateDecorations(); 122 | } 123 | }); 124 | vscode.window.onDidChangeActiveTextEditor( 125 | editor => { 126 | activeEditor = editor; 127 | if (activeEditor) { 128 | triggerUpdateDecorations( 129 | vscode.workspace.getConfiguration('phpParameterHint').get('textEditorChangeDelay') 130 | ); 131 | } 132 | }, 133 | null, 134 | context.subscriptions 135 | ); 136 | const handleVisibleRangesChange = debounce(() => { 137 | if ( 138 | activeEditor && 139 | vscode.workspace.getConfiguration('phpParameterHint').get('hintOnlyVisibleRanges') 140 | ) { 141 | triggerUpdateDecorations(0); 142 | } 143 | }, 333); 144 | vscode.window.onDidChangeTextEditorVisibleRanges( 145 | handleVisibleRangesChange, 146 | null, 147 | context.subscriptions 148 | ); 149 | vscode.window.onDidChangeTextEditorSelection( 150 | () => { 151 | if ( 152 | activeEditor && 153 | vscode.workspace.getConfiguration('phpParameterHint').get('hintOnlyLine') 154 | ) { 155 | triggerUpdateDecorations(0); 156 | } 157 | }, 158 | null, 159 | context.subscriptions 160 | ); 161 | vscode.workspace.onDidChangeTextDocument( 162 | debounce(event => { 163 | if ( 164 | activeEditor && 165 | event.document === activeEditor.document && 166 | vscode.workspace.getConfiguration('phpParameterHint').get('onChange') 167 | ) { 168 | triggerUpdateDecorations( 169 | vscode.workspace.getConfiguration('phpParameterHint').get('changeDelay') 170 | ); 171 | } 172 | }, 333), 173 | null, 174 | context.subscriptions 175 | ); 176 | vscode.workspace.onDidSaveTextDocument( 177 | document => { 178 | if ( 179 | activeEditor && 180 | activeEditor.document === document && 181 | vscode.workspace.getConfiguration('phpParameterHint').get('onSave') 182 | ) { 183 | triggerUpdateDecorations( 184 | vscode.workspace.getConfiguration('phpParameterHint').get('saveDelay') 185 | ); 186 | } 187 | }, 188 | null, 189 | context.subscriptions 190 | ); 191 | Commands.registerCommands(); 192 | 193 | if (activeEditor) { 194 | tryInitial(initialNrTries); 195 | } 196 | } 197 | 198 | module.exports = { 199 | activate 200 | }; 201 | -------------------------------------------------------------------------------- /src/parser.spec.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require('mocha'); 2 | const { expect } = require('chai'); 3 | const Parser = require('./parser'); 4 | 5 | describe('Parser', () => { 6 | const parser = new Parser(true); 7 | 8 | describe('parse', () => {}); 9 | it('should correctly parse and store the function groups from text', () => { 10 | // with function groups 11 | const text = ` { 55 | // without function groups 56 | const text = ` { 64 | // with html 65 | const text = ` 66 |
List:
67 | `; 68 | parser.parse(text); 69 | const { functionGroups } = parser; 70 | const expectedFunctionGroups = [ 71 | { 72 | name: '', 73 | args: [ 74 | { 75 | key: 0, 76 | start: { 77 | line: 1, 78 | character: 31 79 | }, 80 | end: { 81 | line: 1, 82 | character: 35 83 | }, 84 | name: '', 85 | kind: 'string' 86 | }, 87 | { 88 | key: 1, 89 | start: { 90 | line: 1, 91 | character: 37 92 | }, 93 | end: { 94 | line: 1, 95 | character: 46 96 | }, 97 | name: '', 98 | kind: 'array' 99 | } 100 | ], 101 | line: 1, 102 | character: 30 103 | } 104 | ]; 105 | expect(functionGroups).to.have.lengthOf(1); 106 | expect(functionGroups).to.deep.equal(expectedFunctionGroups); 107 | }); 108 | it('should save all the function groups from the text', () => { 109 | // with multiple function groups 110 | const text = ` { 189 | const text = ` { 194 | parser.parse(text); 195 | }).to.throw(); 196 | }); 197 | it('should correctly parse the text and save the function groups when php short tags are used', () => { 198 | // with short tags 199 | const text = ` 200 |Name: =ucfrst('unknown')?>
201 |Age: echo abs(-35)?>
202 | `; 203 | parser.parse(text); 204 | const { functionGroups } = parser; 205 | const expectedFunctionGroups = [ 206 | { 207 | name: '', 208 | args: [ 209 | { 210 | key: 0, 211 | start: { 212 | line: 1, 213 | character: 25 214 | }, 215 | end: { 216 | line: 1, 217 | character: 34 218 | }, 219 | name: '', 220 | kind: 'string' 221 | } 222 | ], 223 | line: 1, 224 | character: 24 225 | }, 226 | { 227 | name: '', 228 | args: [ 229 | { 230 | key: 0, 231 | start: { 232 | line: 2, 233 | character: 26 234 | }, 235 | end: { 236 | line: 2, 237 | character: 29 238 | }, 239 | name: '', 240 | kind: 'unary' 241 | } 242 | ], 243 | line: 2, 244 | character: 25 245 | } 246 | ]; 247 | expect(functionGroups).to.have.lengthOf(2); 248 | expect(functionGroups).to.deep.equal(expectedFunctionGroups); 249 | }); 250 | }); 251 | -------------------------------------------------------------------------------- /src/commands.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | const vscode = require('vscode'); 3 | 4 | // default = 'disabled' - only name 5 | const showTypeEnum = Object.freeze({ 0: 'disabled', 1: 'type and name', 2: 'type' }); 6 | 7 | class Commands { 8 | static registerCommands() { 9 | const messageHeader = 'PHP Parameter Hint: '; 10 | const hideMessageAfterMs = 3000; 11 | let message; 12 | 13 | // Command to hide / show hints 14 | vscode.commands.registerCommand('phpParameterHint.toggle', async () => { 15 | const currentState = vscode.workspace.getConfiguration('phpParameterHint').get('enabled'); 16 | message = `${messageHeader} Hints ${currentState ? 'disabled' : 'enabled'}`; 17 | 18 | await vscode.workspace 19 | .getConfiguration('phpParameterHint') 20 | .update('enabled', !currentState, true); 21 | vscode.window.setStatusBarMessage(message, hideMessageAfterMs); 22 | }); 23 | 24 | // Command to toggle hinting on text change 25 | vscode.commands.registerCommand('phpParameterHint.toggleOnChange', async () => { 26 | const currentState = vscode.workspace.getConfiguration('phpParameterHint').get('onChange'); 27 | message = `${messageHeader} Hint on change ${currentState ? 'disabled' : 'enabled'}`; 28 | 29 | await vscode.workspace 30 | .getConfiguration('phpParameterHint') 31 | .update('onChange', !currentState, true); 32 | vscode.window.setStatusBarMessage(message, hideMessageAfterMs); 33 | }); 34 | 35 | // Command to toggle hinting on document save 36 | vscode.commands.registerCommand('phpParameterHint.toggleOnSave', async () => { 37 | const currentState = vscode.workspace.getConfiguration('phpParameterHint').get('onSave'); 38 | message = `${messageHeader} Hint on save ${currentState ? 'disabled' : 'enabled'}`; 39 | 40 | await vscode.workspace 41 | .getConfiguration('phpParameterHint') 42 | .update('onSave', !currentState, true); 43 | vscode.window.setStatusBarMessage(message, hideMessageAfterMs); 44 | }); 45 | 46 | // Command to toggle between showing param name, name and type and only type 47 | const showTypeKeys = Object.keys(showTypeEnum).map(key => parseInt(key, 10)); 48 | const minShowType = Math.min(...showTypeKeys); 49 | const maxShowType = Math.max(...showTypeKeys); 50 | vscode.commands.registerCommand('phpParameterHint.toggleTypeName', async () => { 51 | const currentShowState = vscode.workspace 52 | .getConfiguration('phpParameterHint') 53 | .get('hintTypeName'); 54 | const newShowState = currentShowState >= maxShowType ? minShowType : currentShowState + 1; 55 | message = `${messageHeader} Hint both name and type: ${showTypeEnum[newShowState]}`; 56 | 57 | await vscode.workspace 58 | .getConfiguration('phpParameterHint') 59 | .update('hintTypeName', newShowState, true); 60 | vscode.window.setStatusBarMessage(message, hideMessageAfterMs); 61 | }); 62 | 63 | // Command to enable/disable hinting only literals 64 | vscode.commands.registerCommand('phpParameterHint.toggleLiterals', async () => { 65 | const currentState = vscode.workspace 66 | .getConfiguration('phpParameterHint') 67 | .get('hintOnlyLiterals'); 68 | message = `${messageHeader} Hint only literals ${currentState ? 'disabled' : 'enabled'}`; 69 | 70 | await vscode.workspace 71 | .getConfiguration('phpParameterHint') 72 | .update('hintOnlyLiterals', !currentState, true); 73 | vscode.window.setStatusBarMessage(message, hideMessageAfterMs); 74 | }); 75 | 76 | // Command to enable/disable hinting only line/selection 77 | vscode.commands.registerCommand('phpParameterHint.toggleLine', async () => { 78 | const currentState = vscode.workspace 79 | .getConfiguration('phpParameterHint') 80 | .get('hintOnlyLine'); 81 | message = `${messageHeader} Hint only line/selection ${ 82 | currentState ? 'disabled' : 'enabled' 83 | }`; 84 | 85 | await vscode.workspace 86 | .getConfiguration('phpParameterHint') 87 | .update('hintOnlyLine', !currentState, true); 88 | vscode.window.setStatusBarMessage(message, hideMessageAfterMs); 89 | }); 90 | 91 | // Command to enable/disable hinting only visible ranges 92 | vscode.commands.registerCommand('phpParameterHint.toggleVisibleRanges', async () => { 93 | const currentState = vscode.workspace 94 | .getConfiguration('phpParameterHint') 95 | .get('hintOnlyVisibleRanges'); 96 | message = `${messageHeader} Hint only visible ranges ${ 97 | currentState ? 'disabled' : 'enabled' 98 | }`; 99 | 100 | await vscode.workspace 101 | .getConfiguration('phpParameterHint') 102 | .update('hintOnlyVisibleRanges', !currentState, true); 103 | vscode.window.setStatusBarMessage(message, hideMessageAfterMs); 104 | }); 105 | 106 | // Command to enable/disable collapsing hints when param name is equal to 107 | // variable name 108 | vscode.commands.registerCommand('phpParameterHint.toggleCollapse', async () => { 109 | const currentState = vscode.workspace 110 | .getConfiguration('phpParameterHint') 111 | .get('collapseHintsWhenEqual'); 112 | message = `${messageHeader} Collapse hints ${currentState ? 'disabled' : 'enabled'}`; 113 | 114 | await vscode.workspace 115 | .getConfiguration('phpParameterHint') 116 | .update('collapseHintsWhenEqual', !currentState, true); 117 | vscode.window.setStatusBarMessage(message, hideMessageAfterMs); 118 | }); 119 | 120 | // Command to enable/disable collapsing type and parameter name when hinting 121 | // types is enabled and they are equal 122 | vscode.commands.registerCommand('phpParameterHint.toggleCollapseType', async () => { 123 | const currentState = vscode.workspace 124 | .getConfiguration('phpParameterHint') 125 | .get('collapseTypeWhenEqual'); 126 | message = `${messageHeader} Collapse type and parameter name ${ 127 | currentState ? 'disabled' : 'enabled' 128 | }`; 129 | 130 | await vscode.workspace 131 | .getConfiguration('phpParameterHint') 132 | .update('collapseTypeWhenEqual', !currentState, true); 133 | vscode.window.setStatusBarMessage(message, hideMessageAfterMs); 134 | }); 135 | 136 | // Show full type, including namespaces instead of the short name 137 | vscode.commands.registerCommand('phpParameterHint.toggleFullType', async () => { 138 | const currentState = vscode.workspace 139 | .getConfiguration('phpParameterHint') 140 | .get('showFullType'); 141 | message = `${messageHeader} Show full type ${currentState ? 'disabled' : 'enabled'}`; 142 | 143 | await vscode.workspace 144 | .getConfiguration('phpParameterHint') 145 | .update('showFullType', !currentState, true); 146 | vscode.window.setStatusBarMessage(message, hideMessageAfterMs); 147 | }); 148 | 149 | // Show dollar sign for parameter name 150 | vscode.commands.registerCommand('phpParameterHint.toggleDollarSign', async () => { 151 | const currentState = vscode.workspace 152 | .getConfiguration('phpParameterHint') 153 | .get('showDollarSign'); 154 | message = `${messageHeader} Show dollar sign for parameter name ${ 155 | currentState ? 'disabled' : 'enabled' 156 | }`; 157 | 158 | await vscode.workspace 159 | .getConfiguration('phpParameterHint') 160 | .update('showDollarSign', !currentState, true); 161 | vscode.window.setStatusBarMessage(message, hideMessageAfterMs); 162 | }); 163 | } 164 | } 165 | 166 | module.exports = { Commands, showTypeEnum }; 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Parameter Hint for Visual Studio Code 2 | 3 | [](https://marketplace.visualstudio.com/items?itemName=robertgr991.php-parameter-hint) 4 | 5 |  6 | 7 | Inserts parameter hints(type, name or both) into function calls to easily understand the parameter role. 8 | 9 | ## Settings 10 | 11 | | Name | Description | Default | 12 | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | 13 | | `phpParameterHint.enabled` | Enable PHP Parameter Hint | true | 14 | | `phpParameterHint.margin` | Hints styling of margin CSS property | 2 | 15 | | `phpParameterHint.verticalPadding` | Top and bottom padding of the hints(px) | 1 | 16 | | `phpParameterHint.horizontalPadding` | Right and left padding of the hints(px) | 4 | 17 | | `phpParameterHint.fontWeight` | Hints styling of font-weight CSS property | "400" | 18 | | `phpParameterHint.borderRadius` | Hints styling of border-radius CSS property in px | 5 | 19 | | `phpParameterHint.opacity` | Hints styling of opacity CSS property | 0.4 | 20 | | `phpParameterHint.fontStyle` | Hints styling of font-style CSS property | "italic" | 21 | | `phpParameterHint.fontSize` | Hints styling of font size CSS property | 12 | 22 | | `phpParameterHint.onSave` | Create parameter hints on document save | true | 23 | | `phpParameterHint.saveDelay` | Delay in ms for on document save run | 250 | 24 | | `phpParameterHint.onChange` | Create parameter hints on document change | false | 25 | | `phpParameterHint.changeDelay` | Delay in ms for on document change run | 100 | 26 | | `phpParameterHint.textEditorChangeDelay` | Delay in ms for on active text editor change | 250 | 27 | | `phpParameterHint.php7` | True if php version is 7.0+, false otherwise | true | 28 | | `phpParameterHint.collapseHintsWhenEqual` | Collapse hint when variable name is the same as parameter name, keep the hint if the argument is passed by reference or if the splat operator is used | false | 29 | | `phpParameterHint.collapseTypeWhenEqual` | Collapse type when it is equal to the variable name | false | 30 | | `phpParameterHint.showFullType` | Show full type, including namespaces instead of the short name | false | 31 | | `phpParameterHint.hintOnlyLiterals` | Show hints only for literals | false | 32 | | `phpParameterHint.hintOnlyLine` | Show hints only for current line/selection | false | 33 | | `phpParameterHint.hintOnlyVisibleRanges` | Show hints only for visible ranges | false | 34 | | `phpParameterHint.hintTypeName` | Hint only name(0 - default) / Hint type and name(1) / Hint type(2) | 0 | 35 | | `phpParameterHint.showDollarSign` | Show dollar sign in front of parameter name | false | 36 | 37 | ## Commands 38 | 39 | | Name | Description | SHORTCUT | 40 | | -------------------------------------- | ----------------------------------------------------------- | ------------------------------- | 41 | | `phpParameterHint.toggle` | Hide / Show Hints | Key: CTRL + K H, Mac: CMD + K H | 42 | | `phpParameterHint.toggleOnChange` | Hide / Show Hints on text change | Key: CTRL + K O, Mac: CMD + K O | 43 | | `phpParameterHint.toggleOnSave` | Hide / Show Hints on document save | Key: CTRL + K S, Mac: CMD + K S | 44 | | `phpParameterHint.toggleLiterals` | Hide / Show Hints only for literals | Key: CTRL + K L, Mac: CMD + K L | 45 | | `phpParameterHint.toggleLine` | Hide / Show Hints only for current line/selection | Key: CTRL + K I, Mac: CMD + K I | 46 | | `phpParameterHint.toggleCollapse` | Hide / Show Hints when variable name matches parameter name | Key: CTRL + K C, Mac: CMD + K C | 47 | | `phpParameterHint.toggleTypeName` | Hint name(default), type and name or only type | Key: CTRL + K T, Mac: CMD + K T | 48 | | `phpParameterHint.toggleCollapseType` | Toggle collapsing type and name when they are equal | Key: CTRL + K Y, Mac: CMD + K Y | 49 | | `phpParameterHint.toggleFullType` | Hide / Show full type name(namespaces including) | Key: CTRL + K U, Mac: CMD + K U | 50 | | `phpParameterHint.toggleVisibleRanges` | Hide / Show Hints only in visible ranges | Key: CTRL + K R, Mac: CMD + K R | 51 | | `phpParameterHint.toggleDollarSign` | Hide / Show dollar sign in front of parameter name | Key: CTRL + K D, Mac: CMD + K D | 52 | 53 | ## Colors 54 | 55 | You can change the default foreground and background colors in the `workbench.colorCustomizations` property in user settings. 56 | 57 | | Name | Description | 58 | | --------------------------------- | ------------------------------------------- | 59 | | `phpParameterHint.hintForeground` | Specifies the foreground color for the hint | 60 | | `phpParameterHint.hintBackground` | Specifies the background color for the hint | 61 | 62 | ## Credits 63 | 64 | [PHP Parser](https://github.com/glayzzle/php-parser) 65 | 66 | [PHP Intelephense](https://github.com/bmewburn/vscode-intelephense) 67 | -------------------------------------------------------------------------------- /src/parameterExtractor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | const vscode = require('vscode'); 3 | const { sameNamePlaceholder, isDefined } = require('./utils'); 4 | const { showTypeEnum } = require('./commands'); 5 | const signature = require('./providers/signature'); 6 | const hover = require('./providers/hover'); 7 | 8 | const isVariadic = label => 9 | label.substr(0, 3) === '...' || label.substr(0, 4) === '&...' || label.substr(0, 4) === '$...'; 10 | 11 | const getVariadic = label => { 12 | if (label.substr(0, 3) === '...') return '...'; 13 | if (label.substr(0, 4) === '&...') return '&...'; 14 | return '$...'; 15 | }; 16 | 17 | const getNameAfterVariadic = label => { 18 | const nameAfterVariadic = 19 | label.substr(0, 3) === '...' ? label.slice(3) : label.replace('...', ''); 20 | 21 | return nameAfterVariadic === '$' ? '' : nameAfterVariadic; 22 | }; 23 | 24 | const filterOnlyTypeLabels = args => 25 | args 26 | .map(label => { 27 | const labels = label.split(' '); 28 | 29 | if (labels.length > 1) { 30 | /** 31 | * Keep the splat operator for rest param even when 32 | * not showing param name to be able to correctly decorate the arguments 33 | */ 34 | return isVariadic(labels[1]) ? `${labels[0]} ${getVariadic(labels[1])}`.trim() : labels[0]; 35 | } 36 | 37 | return ''; 38 | }) 39 | .filter(label => label !== ''); 40 | 41 | const resolveTypeHint = (showTypeState, args, showTypes) => { 42 | const newArgs = args.map(arg => { 43 | // eslint-disable-next-line prefer-const 44 | let [type, label] = arg.split(' '); 45 | 46 | if (!isDefined(label)) { 47 | return type; 48 | } 49 | 50 | let finalType = type; 51 | const showFullType = vscode.workspace.getConfiguration('phpParameterHint').get('showFullType'); 52 | 53 | if (!showFullType) { 54 | /** 55 | * Keep only the short name of the type 56 | * stripping away any namespace 57 | */ 58 | const splittedType = type.split('\\'); 59 | finalType = splittedType[splittedType.length - 1]; 60 | } 61 | 62 | if (type.indexOf('?') === 0 && finalType.indexOf('|null') === -1) { 63 | // If param is optional and this is not already set 64 | finalType = `${finalType}|null`; 65 | } 66 | 67 | finalType = finalType.replace('?', ''); 68 | 69 | if (finalType[0] === '\\') { 70 | finalType = finalType.slice(1); 71 | } 72 | 73 | const collapseTypeWhenEqual = vscode.workspace 74 | .getConfiguration('phpParameterHint') 75 | .get('collapseTypeWhenEqual'); 76 | const cleanLabel = label.slice(1); // without dollar sign 77 | 78 | if (collapseTypeWhenEqual && label[0] === '$' && finalType === cleanLabel) { 79 | return showTypes === 'type' ? `${finalType} ${label}` : `${label}`; 80 | } 81 | 82 | return `${finalType} ${label}`; 83 | }); 84 | 85 | return showTypeState === 'type' ? filterOnlyTypeLabels(newArgs) : newArgs; 86 | }; 87 | 88 | const createHintText = (arg, collapseHintsWhenEqual) => { 89 | if (collapseHintsWhenEqual && arg.name.indexOf('$') === -1) { 90 | if (arg.name === sameNamePlaceholder) { 91 | return null; 92 | } 93 | 94 | return `${arg.name.replace(sameNamePlaceholder, '').trim()}:`; 95 | } 96 | 97 | const showDollarSign = vscode.workspace 98 | .getConfiguration('phpParameterHint') 99 | .get('showDollarSign'); 100 | 101 | return `${arg.name.replace('$', showDollarSign ? '$' : '').replace('& ', '&')}:`; 102 | }; 103 | 104 | /** 105 | * Get the parameter name 106 | * 107 | * @param {Map