diff --git a/.scripts/samples.js b/.scripts/samples.js index 4c6c2b80..a3e034b7 100644 --- a/.scripts/samples.js +++ b/.scripts/samples.js @@ -184,6 +184,13 @@ const samples = [ guide: null, apis: [`languages.registerCodeLensProvider`, `CodeLensProvider`, `CodeLens`], contributions: [] + }, + { + description: 'Call Hierarchy Sample', + path: 'call-hierarchy-sample', + guide: null, + apis: [`languages.registerCallHierarchyProvider`, `CallHierarchyProvider`, `CallHierarchyItem`, `CallHierarchyOutgoingCall`, `CallHierarchyIncomingCall`], + contributions: [] } ] diff --git a/call-hierarchy-sample/.gitignore b/call-hierarchy-sample/.gitignore new file mode 100644 index 00000000..5fe00fea --- /dev/null +++ b/call-hierarchy-sample/.gitignore @@ -0,0 +1,4 @@ +out +node_modules +.vscode-test/ +*.vsix diff --git a/call-hierarchy-sample/.vscode/launch.json b/call-hierarchy-sample/.vscode/launch.json new file mode 100644 index 00000000..15fcee77 --- /dev/null +++ b/call-hierarchy-sample/.vscode/launch.json @@ -0,0 +1,22 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "npm: watch" + } + ] +} \ No newline at end of file diff --git a/call-hierarchy-sample/.vscode/settings.json b/call-hierarchy-sample/.vscode/settings.json new file mode 100644 index 00000000..e46111f1 --- /dev/null +++ b/call-hierarchy-sample/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.insertSpaces": false +} \ No newline at end of file diff --git a/call-hierarchy-sample/.vscode/tasks.json b/call-hierarchy-sample/.vscode/tasks.json new file mode 100644 index 00000000..241aa6d9 --- /dev/null +++ b/call-hierarchy-sample/.vscode/tasks.json @@ -0,0 +1,20 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/call-hierarchy-sample/.vscodeignore b/call-hierarchy-sample/.vscodeignore new file mode 100644 index 00000000..85571789 --- /dev/null +++ b/call-hierarchy-sample/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +.vscode-test/** +out/test/** +out/**/*.map +src/** +.gitignore +tsconfig.json +vsc-extension-quickstart.md +tslint.json \ No newline at end of file diff --git a/call-hierarchy-sample/README.md b/call-hierarchy-sample/README.md new file mode 100644 index 00000000..4b9d87ce --- /dev/null +++ b/call-hierarchy-sample/README.md @@ -0,0 +1,34 @@ +# Call Hierarchy Provider Sample + +This sample shows the **Call Hierarchy** in action based on a simple food pyramid model defined using simple subject~verb~object syntax. + +![Sample](demo.gif) + +## VS Code API + +### `vscode` module + +- [`languages.registerCallHierarchyProvider`](https://code.visualstudio.com/api/references/vscode-api#languages.registerCallHierarchyProvider) +- [`CallHierarchyProvider`](https://code.visualstudio.com/api/references/vscode-api#CallHierarchyProvider) + +## Running the Sample + +Start the extension in the debugger and it automatically opens a file that is ready for the right-click > Peek Call Hierarchy. Otherwise, it can be testing by creating a file with extension `.txt` and pasting following text: + +```plaintext +Coyote eats deer. +Deer eats plants. +Coyote eats lizard. +Lizard eats bird. +Lizard eats frog. +Lizard eats butterfly. +Bird eats seeds. +Frog eats insects. +Butterfly eats fruit. +``` + +Right click on a noun or a verb and select _Peek Call Hierarchy_. + +## Contributing to the Sample and Testing the Sample + +Run the _Run Extension Tests_ configuration and verify in the Debug Console that all tests are passing. \ No newline at end of file diff --git a/call-hierarchy-sample/demo.gif b/call-hierarchy-sample/demo.gif new file mode 100644 index 00000000..599421e8 Binary files /dev/null and b/call-hierarchy-sample/demo.gif differ diff --git a/call-hierarchy-sample/package-lock.json b/call-hierarchy-sample/package-lock.json new file mode 100644 index 00000000..87f786aa --- /dev/null +++ b/call-hierarchy-sample/package-lock.json @@ -0,0 +1,323 @@ +{ + "name": "call-hierarchy-sample", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@types/node": { + "version": "10.14.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.17.tgz", + "integrity": "sha512-p/sGgiPaathCfOtqu2fx5Mu1bcjuP8ALFg4xpGgNkcin7LwRyzUKniEHBKdcE1RPsenq5JVPIpMTJSygLboygQ==", + "dev": true + }, + "@types/vscode": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.40.0.tgz", + "integrity": "sha512-5kEIxL3qVRkwhlMerxO7XuMffa+0LBl+iG2TcRa0NsdoeSFLkt/9hJ02jsi/Kvc6y8OVF2N2P2IHP5S4lWf/5w==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "resolve": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", + "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + }, + "tslint": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.19.0.tgz", + "integrity": "sha512-1LwwtBxfRJZnUvoS9c0uj8XQtAnyhWr9KlNvDIdB+oXyT+VpsOAaEhEgKi1HrZ8rq0ki/AAnbGSv4KM6/AfVZw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "typescript": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.2.tgz", + "integrity": "sha512-lmQ4L+J6mnu3xweP8+rOrUwzmN+MRAj7TgtJtDaXE5PMyX2kCrklhg3rvOsOIfNeAWMQWO2F1GPc1kMD2vLAfw==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/call-hierarchy-sample/package.json b/call-hierarchy-sample/package.json new file mode 100644 index 00000000..52f7f027 --- /dev/null +++ b/call-hierarchy-sample/package.json @@ -0,0 +1,30 @@ +{ + "name": "call-hierarchy-sample", + "displayName": "call-hierarchy-sample", + "description": "Call hierarchy provider sample", + "version": "0.0.1", + "publisher": "vscode-samples", + "repository": "https://github.com/Microsoft/vscode-extension-samples/call-hierarchy-sample", + "engines": { + "vscode": "^1.40.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onLanguage:plaintext" + ], + "main": "./out/extension.js", + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "lint": "tslint -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "@types/node": "^10.14.17", + "@types/vscode": "^1.40.0", + "tslint": "^5.16.0", + "typescript": "^3.5.1" + } +} diff --git a/call-hierarchy-sample/sample.txt b/call-hierarchy-sample/sample.txt new file mode 100644 index 00000000..7f306cbd --- /dev/null +++ b/call-hierarchy-sample/sample.txt @@ -0,0 +1,11 @@ +# Right click on a term below and select `Peek Call Hierarchy`. + +Coyote eats deer. +Deer eats plants. +Coyote eats lizard. +Lizard eats bird. +Lizard eats frog. +Lizard eats butterfly. +Bird eats seeds. +Frog eats insects. +Butterfly eats fruit. \ No newline at end of file diff --git a/call-hierarchy-sample/src/FoodPyramidHierarchyProvider.ts b/call-hierarchy-sample/src/FoodPyramidHierarchyProvider.ts new file mode 100644 index 00000000..0c2afeb0 --- /dev/null +++ b/call-hierarchy-sample/src/FoodPyramidHierarchyProvider.ts @@ -0,0 +1,155 @@ +import { + Range, Position, CallHierarchyProvider, TextDocument, CancellationToken, CallHierarchyItem, + SymbolKind, ProviderResult, CallHierarchyIncomingCall, CallHierarchyOutgoingCall, workspace, Uri +} from 'vscode'; +import { FoodPyramid, FoodRelation } from './model'; + +export class FoodPyramidHierarchyProvider implements CallHierarchyProvider { + + prepareCallHierarchy(document: TextDocument, position: Position, token: CancellationToken): CallHierarchyItem | undefined { + let range = document.getWordRangeAtPosition(position); + if (range) { + let word = document.getText(range); + return this.createCallHierarchyItem(word, '', document, range); + } else { + return undefined; + } + } + + async provideCallHierarchyOutgoingCalls(item: CallHierarchyItem, token: CancellationToken): Promise { + let document = await workspace.openTextDocument(item.uri); + let parser = new FoodPyramidParser(); + parser.parse(document); + let model = parser.getModel(); + let originRelation = model.getRelationAt(item.range); + + let outgoingCallItems: CallHierarchyOutgoingCall[] = []; + + if (model.isVerb(item.name)) { + let outgoingCalls = model.getVerbRelations(item.name) + .filter(relation => relation.subject === originRelation!.subject); + + outgoingCalls.forEach(relation => { + let outgoingCallRange = relation.getRangeOf(relation.object); + let verbItem = this.createCallHierarchyItem(relation.object, 'noun', document, outgoingCallRange); + let outgoingCallItem = new CallHierarchyOutgoingCall(verbItem, [outgoingCallRange]); + outgoingCallItems.push(outgoingCallItem); + }); + } + else if (model.isNoun(item.name)) { + let outgoingCallMap = groupBy(model.getSubjectRelations(item.name), relation => relation.verb); + + outgoingCallMap.forEach((relations, verb) => { + let outgoingCallRanges = relations.map(relation => relation.getRangeOf(verb)); + let verbItem = this.createCallHierarchyItem(verb, 'verb', document, outgoingCallRanges[0]); + let outgoingCallItem = new CallHierarchyOutgoingCall(verbItem, outgoingCallRanges); + outgoingCallItems.push(outgoingCallItem); + }); + } + + return outgoingCallItems; + } + + async provideCallHierarchyIncomingCalls(item: CallHierarchyItem, token: CancellationToken): Promise { + let document = await workspace.openTextDocument(item.uri); + let parser = new FoodPyramidParser(); + parser.parse(document); + let model = parser.getModel(); + let originRelation = model.getRelationAt(item.range); + + let outgoingCallItems: CallHierarchyIncomingCall[] = []; + + if (model.isVerb(item.name)) { + let outgoingCalls = model.getVerbRelations(item.name) + .filter(relation => relation.object === originRelation!.object); + + outgoingCalls.forEach(relation => { + let outgoingCallRange = relation.getRangeOf(relation.subject); + let verbItem = this.createCallHierarchyItem(relation.subject, 'noun', document, outgoingCallRange); + let outgoingCallItem = new CallHierarchyIncomingCall(verbItem, [outgoingCallRange]); + outgoingCallItems.push(outgoingCallItem); + }); + } + else if (model.isNoun(item.name)) { + let outgoingCallMap = groupBy(model.getObjectRelations(item.name), relation => relation.verb); + + outgoingCallMap.forEach((relations, verb) => { + let outgoingCallRanges = relations.map(relation => relation.getRangeOf(verb)); + let verbItem = this.createCallHierarchyItem(verb, 'verb-inverted', document, outgoingCallRanges[0]); + let outgoingCallItem = new CallHierarchyIncomingCall(verbItem, outgoingCallRanges); + outgoingCallItems.push(outgoingCallItem); + }); + } + + return outgoingCallItems; + } + + private createCallHierarchyItem(word: string, type: string, document: TextDocument, range: Range): CallHierarchyItem { + return new CallHierarchyItem(SymbolKind.Object, word, `(${type})`, document.uri, range, range); + } + + deriveCalledItem(item: CallHierarchyItem, called: string, document: TextDocument): CallHierarchyOutgoingCall { + const range = this.rangeOf(called, document); + let calledItem = new CallHierarchyItem(item.kind, called, called, item.uri, range, range); + return new CallHierarchyOutgoingCall(calledItem, this.allRangesOf(called, document)); + } + + rangeOf(word: string, document: TextDocument): Range { + let match = new RegExp("\\b" + word + "\\b").exec(document.getText()); + let offset = match!.index; + return this.toRange(document, offset, word); + } + + allRangesOf(word: string, document: TextDocument): Range[] { + let pattern = new RegExp("\b" + word + "\b"); + let ranges: Range[] = []; + + var match: RegExpExecArray | null; + while (match = pattern.exec(document.getText())) { + ranges.push(this.toRange(document, match.index, word)); + } + return ranges; + } + + private toRange(document: TextDocument, offset: number, word: string) { + let position = document.positionAt(offset); + return new Range(position, position.translate({ characterDelta: word.length })); + } +} + +/** + * Sample parser of the document text into the [FoodPyramid](#FoodPyramid) model. + */ +class FoodPyramidParser { + private model = new FoodPyramid(); + + getModel(): FoodPyramid { + return this.model; + } + + parse(textDocument: TextDocument): void { + let pattern = /^(\w+)\s+(\w+)\s+(\w+).$/gm; + let match: RegExpExecArray | null; + while (match = pattern.exec(textDocument.getText())) { + let startPosition = textDocument.positionAt(match.index); + let range = new Range(startPosition, startPosition.translate({ characterDelta: match[0].length })); + this.model.addRelation(new FoodRelation(match[1], match[2], match[3], match[0], range)); + } + } +} + +/** + * Groups array items by a field defined using a key selector. + * @param array array to be grouped + * @param keyGetter grouping key selector + */ +function groupBy(array: Array, keyGetter: (value: V) => K): Map { + const map = new Map(); + array.forEach((item) => { + const key = keyGetter(item); + const groupForKey = map.get(key) || []; + groupForKey.push(item); + map.set(key, groupForKey); + }); + return map; +} diff --git a/call-hierarchy-sample/src/extension.ts b/call-hierarchy-sample/src/extension.ts new file mode 100644 index 00000000..ef704543 --- /dev/null +++ b/call-hierarchy-sample/src/extension.ts @@ -0,0 +1,29 @@ +// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import { FoodPyramidHierarchyProvider } from './FoodPyramidHierarchyProvider'; + +// this method is called when your extension is activated +// your extension is activated the very first time the command is executed +export function activate(context: vscode.ExtensionContext) { + // Use the console to output diagnostic information (console.log) and errors (console.error) + // This line of code will only be executed once when your extension is activated + console.log('Congratulations, your extension "call-hierarchy-sample" is now active!'); + + let disposable = vscode.languages.registerCallHierarchyProvider('plaintext', new FoodPyramidHierarchyProvider()); + + context.subscriptions.push(disposable); + + showSampleText(context); +} + +async function showSampleText(context: vscode.ExtensionContext): Promise { + fs.readFile(context.asAbsolutePath('sample.txt'), async (err, sampleText) => { + let doc = await vscode.workspace.openTextDocument({ language: 'plaintext', content: sampleText.toString("utf-8") }); + vscode.window.showTextDocument(doc); + }); +} + +// this method is called when your extension is deactivated +export function deactivate() { } diff --git a/call-hierarchy-sample/src/model.ts b/call-hierarchy-sample/src/model.ts new file mode 100644 index 00000000..f9da9ef4 --- /dev/null +++ b/call-hierarchy-sample/src/model.ts @@ -0,0 +1,91 @@ +import { + Range, Position, CallHierarchyProvider, TextDocument, CancellationToken, CallHierarchyItem, + SymbolKind, ProviderResult, CallHierarchyIncomingCall, CallHierarchyOutgoingCall, workspace, Uri +} from 'vscode'; + +/** + * Sample model of what the text in the document contains. + */ +export class FoodPyramid { + private relations: FoodRelation[] = []; + private nouns = new Set(); + private verbs = new Set(); + + getRelationAt(wordRange: Range): FoodRelation | undefined { + return this.relations.find(relation => relation.range.contains(wordRange)); + } + + addRelation(relation: FoodRelation): void { + this.relations.push(relation); + this.nouns.add(relation.object).add(relation.subject); + this.verbs.add(relation.verb); + } + + isVerb(name: string): boolean { + return this.verbs.has(name.toLowerCase()); + } + + isNoun(name: string): boolean { + return this.nouns.has(name.toLowerCase()); + } + + getVerbRelations(verb: string): FoodRelation[] { + return this.relations + .filter(relation => relation.verb === verb.toLowerCase()); + } + + getNounRelations(noun: string): FoodRelation[] { + return this.relations + .filter(relation => relation.involves(noun)); + } + + getSubjectRelations(subject: string): FoodRelation[] { + return this.relations + .filter(relation => relation.subject === subject.toLowerCase()); + } + + getObjectRelations(object: string): FoodRelation[] { + return this.relations + .filter(relation => relation.object === object.toLowerCase()); + } +} + +/** + * Model element. + */ +export class FoodRelation { + private _subject: string; + private _verb: string; + private _object: string; + + constructor(subject: string, verb: string, object: string, + private readonly originalText: string, public readonly range: Range) { + + this._subject = subject.toLowerCase(); + this._verb = verb.toLowerCase(); + this._object = object.toLowerCase(); + } + + get subject(): string { + return this._subject; + } + + get object(): string { + return this._object; + } + + get verb(): string { + return this._verb; + } + + involves(noun: string): boolean { + let needle = noun.toLowerCase(); + return this._subject === needle || this._object === needle; + } + + getRangeOf(word: string): Range { + let indexOfWord = new RegExp("\\b" + word + "\\b", "i").exec(this.originalText)!.index; + return new Range(this.range.start.translate({ characterDelta: indexOfWord }), + this.range.start.translate({ characterDelta: indexOfWord + word.length })); + } +} diff --git a/call-hierarchy-sample/tsconfig.json b/call-hierarchy-sample/tsconfig.json new file mode 100644 index 00000000..aa034c3f --- /dev/null +++ b/call-hierarchy-sample/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "sourceMap": true, + "rootDir": "src", + "strict": true + }, + "exclude": ["node_modules", ".vscode-test"] +} diff --git a/call-hierarchy-sample/tslint.json b/call-hierarchy-sample/tslint.json new file mode 100644 index 00000000..0ab0ca6e --- /dev/null +++ b/call-hierarchy-sample/tslint.json @@ -0,0 +1,6 @@ +{ + "rules": { + "indent": [true, "tabs"], + "semicolon": [true, "always"] + } +} \ No newline at end of file diff --git a/progress-sample/package-lock.json b/progress-sample/package-lock.json index 9500a66c..c1815c91 100644 --- a/progress-sample/package-lock.json +++ b/progress-sample/package-lock.json @@ -308,9 +308,9 @@ } }, "typescript": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", + "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", "dev": true }, "wrappy": { diff --git a/progress-sample/src/extension.ts b/progress-sample/src/extension.ts index db3c9880..67146f11 100644 --- a/progress-sample/src/extension.ts +++ b/progress-sample/src/extension.ts @@ -2,41 +2,82 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +'use strict'; -import { ExtensionContext, StatusBarAlignment, window, StatusBarItem, Selection, workspace, TextEditor, commands, ProgressLocation } from 'vscode'; +import { ExtensionContext, window, commands, ProgressLocation, CancellationToken, Progress } from 'vscode'; +import { spawn, spawnSync } from 'child_process'; export function activate(context: ExtensionContext) { - context.subscriptions.push(commands.registerCommand('extension.startTask', () => { + context.subscriptions.push(commands.registerCommand('extension.askMode', () => { + return window.showQuickPick(['sync', 'async'], { placeHolder: 'Pick mode...' }); + })); + context.subscriptions.push(commands.registerCommand('extension.startTask', async () => { + let mode = await commands.executeCommand('extension.askMode'); window.withProgress({ location: ProgressLocation.Notification, - title: "I am long running!", + title: "I am long running", cancellable: true - }, (progress, token) => { + }, async (progress, token) => { token.onCancellationRequested(() => { console.log("User canceled the long running operation"); }); - progress.report({ increment: 0 }); - - setTimeout(() => { - progress.report({ increment: 10, message: "I am long running! - still going..." }); - }, 1000); - - setTimeout(() => { - progress.report({ increment: 40, message: "I am long running! - still going even more..." }); - }, 2000); - - setTimeout(() => { - progress.report({ increment: 50, message: "I am long running! - almost there..." }); - }, 3000); - - var p = new Promise(resolve => { - setTimeout(() => { - resolve(); - }, 5000); - }); - - return p; + switch (mode) { + case undefined: + return; // canceled by the user + case 'sync': + return spawnSomethingSync(token); + case 'async': + default: + return spawnSomethingAsync(progress, token); + } }); })); } + +/** + * Synchronous approach + * @param _token cancellation token (unused in the sync approach) + */ +function spawnSomethingSync(_token: CancellationToken): Promise { + return new Promise(resolve => { + console.log('Started...'); + let child = spawnSync('cmd', ['/c', 'dir', '/S'], { cwd: 'c:\\', encoding: 'utf8' }); + console.log(`stdout: ${child.stdout.slice(0, 1000)}`); // otherwise it is too big for the console + resolve(); + }); +} + +/** + * Asynchronous approach + * @param token cancellation token (triggered by the cancel button on the UI) + */ +function spawnSomethingAsync(progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken): Promise { + return new Promise((resolve, reject) => { + if (token.isCancellationRequested) { + return; + } + + var progressUpdate = 'Starting up...'; + const interval = setInterval(() => progress.report({ message: progressUpdate }), 500); + + let childProcess = spawn('cmd', ['/c', 'dir', '/S'], { cwd: 'C:\\Users\\jdolejsi\\AppData\\' }) + .on("close", (code, signal) => { + console.log(`Closed: ${code} ${signal}`); + if (childProcess.killed) { console.log('KILLED'); } + resolve(); + clearInterval(interval); + }) + .on("error", err => { + reject(err); + }); + + childProcess.stdout + .on("data", (chunk: string | Buffer) => { + console.log(`stdout: ${chunk}`); + progressUpdate = chunk.toString('utf8', 0, 50).replace(/[\r\n]/g, ''); + }); + + token.onCancellationRequested(_ => childProcess.kill()); + }); +} \ No newline at end of file