diff --git a/.gitignore b/.gitignore index 77c07c0..fb7eabe 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ node_modules/ .idea/ build.vsix Scratch.fsx -Scratch.ts \ No newline at end of file +Scratch.ts +fsharp-language-server.sln.DotSettings.user diff --git a/client/extension.ts b/client/extension.ts index 96837ab..27d5d05 100644 --- a/client/extension.ts +++ b/client/extension.ts @@ -26,8 +26,10 @@ export function activate(context: ExtensionContext) { transport: TransportKind.stdio } if (debugMode) { + const commandName = process.platform === 'win32' ? 'dotnet.exe' : 'dotnet'; + serverOptions = { - command: findInPath('dotnet'), + command: findInPath(commandName), args: ['run', '--project', 'src/FSharpLanguageServer'], transport: TransportKind.stdio, options: { cwd: context.extensionPath } diff --git a/package-lock.json b/package-lock.json index eb50789..2080995 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,353 +1,100 @@ { "name": "fsharp-language-server", "version": "0.1.27", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, - "dependencies": { - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true + "packages": { + "": { + "version": "0.1.27", + "license": "MIT", + "dependencies": { + "@types/vscode": "^1.55.0", + "typescript": "^4.2.4", + "vscode-languageclient": "^4.2.1" + }, + "devDependencies": { + "@types/node": "^14.14.37", + "@types/vscode": "^1.24.0", + "typescript": "^4.2.4" + }, + "engines": { + "vscode": "^1.24.0" + } }, - "@types/node": { - "version": "10.17.59", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.59.tgz", - "integrity": "sha512-7Uc8IRrL8yZz5ti45RaFxpbU8TxlzdC3HvxV+hOWo1EyLsuKv/w7y0n+TwZzwL3vdx3oZ2k3ubxPq131hNtXyg==", + "node_modules/@types/node": { + "version": "14.14.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz", + "integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==", "dev": true }, - "@types/vscode": { + "node_modules/@types/vscode": { "version": "1.55.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.55.0.tgz", "integrity": "sha512-49hysH7jneTQoSC8TWbAi7nKK9Lc5osQNjmDHVosrcU8o3jecD9GrK0Qyul8q4aGPSXRfNGqIp9CBdb13akETg==" }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "big-integer": { - "version": "1.6.48", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", - "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", - "dev": true - }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "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" - } - }, - "buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true - }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true - }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, - "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 - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" + "node_modules/typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } + "engines": { + "node": ">=4.2.0" } }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "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" - } - }, - "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", - "dev": true - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "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 - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, - "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": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" + "node_modules/vscode-jsonrpc": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz", + "integrity": "sha512-T24Jb5V48e4VgYliUXMnZ379ItbrXgOimweKaJshD84z+8q7ZOZjJan0MeDe+Ugb+uqERDVV8SBmemaGMSMugA==", + "engines": { + "node": ">=4.0.0 || >=6.0.0" } }, - "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 - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "node_modules/vscode-languageclient": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-4.2.1.tgz", + "integrity": "sha512-zeixIe2MiKPHiSNjEUmRhWFiNCGUwUNvefBiA9diZc7fXE8DX+AhfwpsOLYauO8Q8C6gW8f9OQvy3Vn2hBvY4g==", + "dependencies": { + "vscode-languageserver-protocol": "^3.8.1" + }, + "engines": { + "vscode": "^1.23" } }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" + "node_modules/vscode-languageserver-protocol": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.8.1.tgz", + "integrity": "sha512-KdeetvQ2JavRiuE9afNrV5+xJZocj7NGPQwH4kpSFw5cp+0wijv87qgXfSEvmwPUaknhMBoSuSrSIG/LRrzWJQ==", + "dependencies": { + "vscode-jsonrpc": "^3.6.2", + "vscode-languageserver-types": "^3.8.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "node_modules/vscode-languageserver-types": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.8.2.tgz", + "integrity": "sha512-2RMkyt1O1czGPCnkjKZWSio2D8oh3XlQ4zi4W2xL8q2Dvi4hB3/DEt+wYyzo4hmE2ZFP0RB8PXyzHyed7p1hbw==" + } + }, + "dependencies": { + "@types/node": { + "version": "14.14.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz", + "integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==", "dev": true }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true + "@types/vscode": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.55.0.tgz", + "integrity": "sha512-49hysH7jneTQoSC8TWbAi7nKK9Lc5osQNjmDHVosrcU8o3jecD9GrK0Qyul8q4aGPSXRfNGqIp9CBdb13akETg==" }, "typescript": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==" }, - "unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, "vscode-jsonrpc": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz", @@ -374,24 +121,6 @@ "version": "3.8.2", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.8.2.tgz", "integrity": "sha512-2RMkyt1O1czGPCnkjKZWSio2D8oh3XlQ4zi4W2xL8q2Dvi4hB3/DEt+wYyzo4hmE2ZFP0RB8PXyzHyed7p1hbw==" - }, - "vscode-test": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.5.2.tgz", - "integrity": "sha512-x9PVfKxF6EInH9iSFGQi0V8H5zIW1fC7RAer6yNQR6sy3WyOwlWkuT3I+wf75xW/cO53hxMi1aj/EvqQfDFOAg==", - "dev": true, - "requires": { - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true } } } diff --git a/package.json b/package.json index 37ad996..1b43703 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,8 @@ "vscode-languageclient": "^4.2.1" }, "devDependencies": { - "@types/node": "^10.17.59", - "vscode-test": "^1.5.2" + "@types/node": "^14.14.37", + "@types/vscode": "^1.24.0", + "typescript": "^4.2.4" } } diff --git a/sample/Net5Console/Net5Console.fsproj b/sample/Net5Console/Net5Console.fsproj index 260a75b..1cd56e8 100644 --- a/sample/Net5Console/Net5Console.fsproj +++ b/sample/Net5Console/Net5Console.fsproj @@ -2,7 +2,7 @@ Exe - net5.0 + netcoreapp3.0 diff --git a/src/FSharpLanguageServer/Conversions.fs b/src/FSharpLanguageServer/Conversions.fs index 0c480b3..c1635c7 100644 --- a/src/FSharpLanguageServer/Conversions.fs +++ b/src/FSharpLanguageServer/Conversions.fs @@ -1,4 +1,4 @@ -module FSharpLanguageServer.Conversions +module FSharpLanguageServer.Conversions open LSP.Log open FSharp.Compiler @@ -8,10 +8,8 @@ open System.IO open LSP.Types open FSharp.Data -module Ast = FSharp.Compiler.Ast - /// Convert an F# Compiler Services 'FSharpErrorInfo' to an LSP 'Range' -let private errorAsRange(err: FSharpErrorInfo): Range = +let private errorAsRange(err: FSharpDiagnostic): Range = { // Got error "The field, constructor or member 'StartLine' is not defined" start = {line=err.StartLineAlternate-1; character=err.StartColumn} @@ -19,34 +17,35 @@ let private errorAsRange(err: FSharpErrorInfo): Range = } /// Convert an F# `Range.pos` to an LSP `Position` -let private asPosition(p: Range.pos): Position = +let private asPosition(p: Text.pos): Position = { line=p.Line-1 character=p.Column } /// Convert an F# `Range.range` to an LSP `Range` -let asRange(r: Range.range): Range = +let asRange(r: Text.range): Range = { start=asPosition r.Start ``end``=asPosition r.End } /// Convert an F# `Range.range` to an LSP `Location` -let private asLocation(l: Range.range): Location = - { +let private asLocation(l: Text.range): Location = + { uri=Uri("file://" + l.FileName) - range = asRange l + range = asRange l } /// Convert an F# Compiler Services 'FSharpErrorSeverity' to an LSP 'DiagnosticSeverity' -let private asDiagnosticSeverity(s: FSharpErrorSeverity): DiagnosticSeverity = - match s with - | FSharpErrorSeverity.Warning -> DiagnosticSeverity.Warning - | FSharpErrorSeverity.Error -> DiagnosticSeverity.Error +let private asDiagnosticSeverity(s: FSharpDiagnosticSeverity): DiagnosticSeverity = + match s with + | FSharpDiagnosticSeverity.Warning -> DiagnosticSeverity.Warning + | FSharpDiagnosticSeverity.Error -> DiagnosticSeverity.Error + | FSharpDiagnosticSeverity.Info -> DiagnosticSeverity.Information /// Convert an F# Compiler Services 'FSharpErrorInfo' to an LSP 'Diagnostic' -let asDiagnostic(err: FSharpErrorInfo): Diagnostic = +let asDiagnostic(err: FSharpDiagnostic): Diagnostic = { range = errorAsRange(err) severity = Some(asDiagnosticSeverity(err.Severity)) @@ -56,18 +55,18 @@ let asDiagnostic(err: FSharpErrorInfo): Diagnostic = } /// Create a Diagnostic -let diagnostic(message: string, range: Range.range, severity: DiagnosticSeverity): Diagnostic = +let diagnostic(message: string, range: Text.range, severity: DiagnosticSeverity): Diagnostic = { range = asRange(range) severity = Some(severity) - code = None - source = None + code = None + source = None message = message } - + /// Some compiler errors have no location in the file and should be displayed at the top of the file -let private hasNoLocation(err: FSharpErrorInfo): bool = - err.StartLineAlternate-1 = 0 && +let private hasNoLocation(err: FSharpDiagnostic): bool = + err.StartLineAlternate-1 = 0 && err.StartColumn = 0 && err.EndLineAlternate-1 = 0 && err.EndColumn = 0 @@ -76,60 +75,60 @@ let private hasNoLocation(err: FSharpErrorInfo): bool = let errorAtTop(message: string): Diagnostic = { range = { start = {line=0; character=0}; ``end`` = {line=0; character=1} } - severity = Some(DiagnosticSeverity.Error) + severity = Some(DiagnosticSeverity.Error) code = None - source = None + source = None message = message } /// Convert a list of F# Compiler Services 'FSharpErrorInfo' to LSP 'Diagnostic' -let asDiagnostics(errors: FSharpErrorInfo seq): Diagnostic list = - [ - for err in errors do - if hasNoLocation(err) then +let asDiagnostics(errors: FSharpDiagnostic seq): Diagnostic list = + [ + for err in errors do + if hasNoLocation(err) then yield errorAtTop(sprintf "%s: %s" err.Subcategory err.Message) else - yield asDiagnostic(err) + yield asDiagnostic(err) ] /// Convert an F# `FSharpToolTipElement` to an LSP `Hover` -let asHover(FSharpToolTipText tips): Hover = - let elements = +let asHover(FSharpToolTipText tips): Hover = + let elements = [ for t in tips do - match t with + match t with | FSharpToolTipElement.CompositionError(e) -> dprintfn "Error rendering tooltip: %s" e - | FSharpToolTipElement.None -> () - | FSharpToolTipElement.Group(elements) -> + | FSharpToolTipElement.None -> () + | FSharpToolTipElement.Group(elements) -> yield! elements ] - let contents = - match elements with + let contents = + match elements with | [] -> [] - | [one] -> - [ yield HighlightedString(one.MainDescription, "fsharp") - match TipFormatter.docComment(one.XmlDoc) with + | [one] -> + [ yield HighlightedString(one.MainDescription, "fsharp") + match TipFormatter.docComment(one.XmlDoc) with | None -> () | Some(markdown) -> yield PlainString(markdown + "\n\n") - match one.Remarks with - | None | Some("") -> () - | Some(remarks) -> + match one.Remarks with + | None | Some("") -> () + | Some(remarks) -> yield PlainString("*" + remarks + "*\n\n") ] - | many -> + | many -> let last = List.last(many) - [ for e in many do + [ for e in many do yield HighlightedString(e.MainDescription, "fsharp") - match TipFormatter.docSummaryOnly(last.XmlDoc) with + match TipFormatter.docSummaryOnly(last.XmlDoc) with | None -> () | Some(markdown) -> yield PlainString(markdown) - match last.Remarks with - | None | Some("") -> () - | Some(remarks) -> + match last.Remarks with + | None | Some("") -> () + | Some(remarks) -> yield PlainString("*" + remarks + "*\n\n") ] {contents=contents; range=None} /// Convert an F# `FSharpGlyph` to an LSP `CompletionItemKind` -let private asCompletionItemKind(k: FSharpGlyph): CompletionItemKind = - match k with +let private asCompletionItemKind(k: FSharpGlyph): CompletionItemKind = + match k with | FSharpGlyph.Class -> CompletionItemKind.Class | FSharpGlyph.Constant -> CompletionItemKind.Constant | FSharpGlyph.Delegate -> CompletionItemKind.Property // ? @@ -153,9 +152,9 @@ let private asCompletionItemKind(k: FSharpGlyph): CompletionItemKind = | FSharpGlyph.Error -> CompletionItemKind.Class // ? /// Convert an F# `FSharpDeclarationListItem` to an LSP `CompletionItem` -let private asCompletionItem(i: FSharpDeclarationListItem): CompletionItem = - { defaultCompletionItem with - label = i.Name +let private asCompletionItem(i: FSharpDeclarationListItem): CompletionItem = + { defaultCompletionItem with + label = i.Name insertText = Some(i.NameInCode) kind = Some(asCompletionItemKind(i.Glyph)) detail = Some(i.FullName) @@ -165,12 +164,12 @@ let private asCompletionItem(i: FSharpDeclarationListItem): CompletionItem = /// Convert an F# `FSharpDeclarationListInfo` to an LSP `CompletionList` /// Used in rendering autocomplete lists -let asCompletionList(ds: FSharpDeclarationListInfo): CompletionList = +let asCompletionList(ds: FSharpDeclarationListInfo): CompletionList = let items = [for i in ds.Items do yield asCompletionItem(i)] {isIncomplete=List.isEmpty(items); items=items} /// Convert an F# `FSharpMethodGroupItemParameter` to an LSP `ParameterInformation` -let private asParameterInformation(p: FSharpMethodGroupItemParameter): ParameterInformation = +let private asParameterInformation(p: FSharpMethodGroupItemParameter): ParameterInformation = { label = p.ParameterName documentation = Some p.Display @@ -178,37 +177,37 @@ let private asParameterInformation(p: FSharpMethodGroupItemParameter): Parameter /// Convert an F# method name + `FSharpMethodGroupItem` to an LSP `SignatureInformation` /// Used in providing signature help after autocompleting -let asSignatureInformation(methodName: string, s: FSharpMethodGroupItem): SignatureInformation = - let doc = match s.Description with - | FSharpToolTipText [FSharpToolTipElement.Group [tip]] -> Some tip.MainDescription - | _ -> - dprintfn "Can't render documentation %A" s.Description - None +let asSignatureInformation(methodName: string, s: FSharpMethodGroupItem): SignatureInformation = + let doc = match s.Description with + | FSharpToolTipText [FSharpToolTipElement.Group [tip]] -> Some tip.MainDescription + | _ -> + dprintfn "Can't render documentation %A" s.Description + None let parameterName(p: FSharpMethodGroupItemParameter) = p.ParameterName let parameterNames = Array.map parameterName s.Parameters { - label = sprintf "%s(%s)" methodName (String.concat ", " parameterNames) - documentation = doc + label = sprintf "%s(%s)" methodName (String.concat ", " parameterNames) + documentation = doc parameters = Array.map asParameterInformation s.Parameters |> List.ofArray } /// Get the lcation where `s` was declared -let declarationLocation(s: FSharpSymbol): Location option = - match s.DeclarationLocation with - | None -> - dprintfn "Symbol %s has no declaration" s.FullName - None +let declarationLocation(s: FSharpSymbol): Location option = + match s.DeclarationLocation with + | None -> + dprintfn "Symbol %s has no declaration" s.FullName + None | Some l -> Some(asLocation(l)) /// Get the location where `s` was used -let useLocation(s: FSharpSymbolUse): Location = +let useLocation(s: FSharpSymbolUse): Location = asLocation(s.RangeAlternate) /// Convert an F# `FSharpNavigationDeclarationItemKind` to an LSP `SymbolKind` /// `FSharpNavigationDeclarationItemKind` is the level of symbol-type information you get when parsing without typechecking -let private asSymbolKind(k: FSharpNavigationDeclarationItemKind): SymbolKind = - match k with +let private asSymbolKind(k: FSharpNavigationDeclarationItemKind): SymbolKind = + match k with | NamespaceDecl -> SymbolKind.Namespace | ModuleFileDecl -> SymbolKind.Module | ExnDecl -> SymbolKind.Class @@ -222,17 +221,17 @@ let private asSymbolKind(k: FSharpNavigationDeclarationItemKind): SymbolKind = /// Convert an F# `NavigationDeclarationItem` to an LSP `SymbolInformation` /// `NavigationDeclarationItem` is the parsed AST representation of a symbol without typechecking /// `container` is present when `d` is part of a module or type -let asSymbolInformation(d: NavigationDeclarationItem, container: NavigationDeclarationItem option): SymbolInformation = - let declarationName(d: NavigationDeclarationItem) = d.Name +let asSymbolInformation(d: NavigationItem, container: NavigationItem option): SymbolInformation = + let declarationName(d: NavigationItem) = d.Name { - name=d.Name - kind=asSymbolKind d.Kind - location=asLocation d.Range + name=d.Name + kind=asSymbolKind d.Kind + location=asLocation d.Range containerName=Option.map declarationName container } /// Convert symbols declared in an .fsi file to a CodeLens that helps the user navigate to the definition -let asGoToImplementation(name: string list, file: FileInfo, range: Range.range): CodeLens = +let asGoToImplementation(name: string list, file: FileInfo, range: Text.range): CodeLens = let jsonFile = JsonValue.String(file.FullName) let jsonName = JsonValue.Array([|for i in name do yield JsonValue.String(i)|]) { @@ -241,13 +240,13 @@ let asGoToImplementation(name: string list, file: FileInfo, range: Range.range): data=JsonValue.Array([|jsonFile; jsonName|]) } -let goToImplementationData(goTo: CodeLens) = - match goTo.data with - | JsonValue.Array([|JsonValue.String(file); JsonValue.Array(jsonNames)|]) -> +let goToImplementationData(goTo: CodeLens) = + match goTo.data with + | JsonValue.Array([|JsonValue.String(file); JsonValue.Array(jsonNames)|]) -> FileInfo(file), [for JsonValue.String(j) in jsonNames do yield j ] -let resolveGoToImplementation(unresolved: CodeLens, file: FileInfo, range: Range.range): CodeLens = - let command = +let resolveGoToImplementation(unresolved: CodeLens, file: FileInfo, range: Text.range): CodeLens = + let command = { title=sprintf "%s(%d)" file.Name range.StartLine command="fsharp.command.goto" @@ -261,8 +260,8 @@ let resolveGoToImplementation(unresolved: CodeLens, file: FileInfo, range: Range } { unresolved with command = Some(command) } -let resolveMissingGoToImplementation(unresolved: CodeLens, file: FileInfo): CodeLens = - let command = +let resolveMissingGoToImplementation(unresolved: CodeLens, file: FileInfo): CodeLens = + let command = { title="Not Found" command="fsharp.command.goto" @@ -276,7 +275,7 @@ let resolveMissingGoToImplementation(unresolved: CodeLens, file: FileInfo): Code } { unresolved with command = Some(command) } -let asRunTest(fsproj: FileInfo, fullyQualifiedName: string list, test: Ast.SynBinding): CodeLens = +let asRunTest(fsproj: FileInfo, fullyQualifiedName: string list, test: SyntaxTree.SynBinding): CodeLens = { range=asRange(test.RangeOfBindingSansRhs) command=Some({ title="Run Test" @@ -285,11 +284,11 @@ let asRunTest(fsproj: FileInfo, fullyQualifiedName: string list, test: Ast.SynBi data=JsonValue.Null } -let asDebugTest(fsproj: FileInfo, fullyQualifiedName: string list, test: Ast.SynBinding): CodeLens = +let asDebugTest(fsproj: FileInfo, fullyQualifiedName: string list, test: SyntaxTree.SynBinding): CodeLens = { range=asRange(test.RangeOfBindingSansRhs) command=Some({ title="Debug Test" command="fsharp.command.test.debug" arguments=[JsonValue.String(fsproj.FullName); JsonValue.String(String.concat "." fullyQualifiedName)] }) data=JsonValue.Null - } \ No newline at end of file + } diff --git a/src/FSharpLanguageServer/FSharpLanguageServer.fsproj b/src/FSharpLanguageServer/FSharpLanguageServer.fsproj index 926dfc5..aded62a 100644 --- a/src/FSharpLanguageServer/FSharpLanguageServer.fsproj +++ b/src/FSharpLanguageServer/FSharpLanguageServer.fsproj @@ -9,6 +9,7 @@ + @@ -18,7 +19,7 @@ - + diff --git a/src/FSharpLanguageServer/Navigation.fs b/src/FSharpLanguageServer/Navigation.fs index 0554b8b..9bbccb6 100644 --- a/src/FSharpLanguageServer/Navigation.fs +++ b/src/FSharpLanguageServer/Navigation.fs @@ -1,147 +1,152 @@ namespace FSharpLanguageServer // Forked from https://github.com/fsharp/FSharp.Compiler.Service/blob/30127fa32cb0306cb81d95836cfee4ea5c116297/src/fsharp/service/ServiceNavigation.fs +// 2021-04-13: +// Small adaptation to the file based on https://github.com/dotnet/fsharp/blob/60a2fa663a3c4aed3f03c8bfc6f5e05b04284f23/src/fsharp/service/ServiceNavigation.fs +// The changes are needed because SyntaxTreeOps functions were no longer exposed by FCS so we had to fork part of that file // TODO this is a bit fuzzy with nesting---I think this would produce wrong answers with pathological examples like: // module Outer1 = -// module Inner = -// val foo: string -// module Outer2 = -// module Inner = +// module Inner = +// val foo: string +// module Outer2 = +// module Inner = // val foo: string open FSharp.Compiler.SourceCodeServices -open FSharp.Compiler.Range -open FSharp.Compiler.Ast +open FSharp.Compiler +open FSharp.Compiler.Text.Range +open FSharp.Compiler.SyntaxTree +open FSharpLanguageServer.SyntaxTreeOps /// Represents an item to be displayed in the navigation bar [] -type NavigationDeclarationItem(uniqueName: string, name: string, kind: FSharpNavigationDeclarationItemKind, glyph: FSharpGlyph, range: range, - bodyRange: range, singleTopLevel: bool, enclosingEntityKind: FSharpEnclosingEntityKind, isAbstract: bool, access: SynAccess option) = - +type NavigationItem(uniqueName: string, name: string, kind: FSharpNavigationDeclarationItemKind, glyph: FSharpGlyph, range: Text.range, + bodyRange: Text.range, singleTopLevel: bool, enclosingEntityKind: FSharpEnclosingEntityKind, isAbstract: bool, access: SynAccess option) = + member x.bodyRange = bodyRange member x.UniqueName = uniqueName member x.Name = name member x.Glyph = glyph member x.Kind = kind member x.Range = range - member x.BodyRange = bodyRange + member x.BodyRange = bodyRange member x.IsSingleTopLevel = singleTopLevel member x.FSharpEnclosingEntityKind = enclosingEntityKind member x.IsAbstract = isAbstract - + member x.Access = access - + member x.WithUniqueName(uniqueName: string) = - NavigationDeclarationItem(uniqueName, name, kind, glyph, range, bodyRange, singleTopLevel, enclosingEntityKind, isAbstract, access) - static member Create(name: string, kind, glyph: FSharpGlyph, range: range, bodyRange: range, singleTopLevel: bool, enclosingEntityKind, isAbstract, access: SynAccess option) = - NavigationDeclarationItem("", name, kind, glyph, range, bodyRange, singleTopLevel, enclosingEntityKind, isAbstract, access) + NavigationItem(uniqueName, name, kind, glyph, range, bodyRange, singleTopLevel, enclosingEntityKind, isAbstract, access) + static member Create(name: string, kind, glyph: FSharpGlyph, range: Text.range, bodyRange: Text.range, singleTopLevel: bool, enclosingEntityKind, isAbstract, access: SynAccess option) = + NavigationItem("", name, kind, glyph, range, bodyRange, singleTopLevel, enclosingEntityKind, isAbstract, access) /// Represents top-level declarations (that should be in the type drop-down) /// with nested declarations (that can be shown in the member drop-down) [] -type FSharpNavigationTopLevelDeclaration = - { Declaration: NavigationDeclarationItem - Nested: NavigationDeclarationItem[] } - +type FSharpNavigationTopLevelDeclaration = + { Declaration: NavigationItem + Nested: NavigationItem[] } + /// Represents result of 'GetNavigationItems' operation - this contains /// all the members and currently selected indices. First level correspond to /// types & modules and second level are methods etc. [] type FSharpNavigationItems(declarations:FSharpNavigationTopLevelDeclaration[]) = member x.Declarations = declarations - -module Navigation = - let unionRangesChecked r1 r2 = if r1 = range.Zero then r2 elif r2 = range.Zero then r1 else unionRanges r1 r2 - - let rangeOfDecls2 f decls = - match (decls |> List.map (f >> (fun (d:NavigationDeclarationItem) -> d.bodyRange))) with + +module Navigation = + let unionRangesChecked r1 r2 = if r1 = Text.range.Zero then r2 elif r2 = Text.range.Zero then r1 else unionRanges r1 r2 + + let rangeOfDecls2 f decls = + match (decls |> List.map (f >> (fun (d: NavigationItem) -> d.bodyRange))) with | hd::tl -> tl |> List.fold unionRangesChecked hd - | [] -> range.Zero - + | [] -> Text.range.Zero + let rangeOfDecls = rangeOfDecls2 fst - let moduleRange (idm:range) others = + let moduleRange (idm:Text.range) others = unionRangesChecked idm.EndRange (rangeOfDecls2 (fun (a, _, _) -> a) others) - + let fldspecRange fldspec = match fldspec with - | UnionCaseFields(flds) -> flds |> List.fold (fun st (Field(_, _, _, _, _, _, _, m)) -> unionRangesChecked m st) range.Zero + | UnionCaseFields(flds) -> flds |> List.fold (fun st (Field(_, _, _, _, _, _, _, m)) -> unionRangesChecked m st) Text.range.Zero | UnionCaseFullType(ty, _) -> ty.Range - + let bodyRange mb decls = unionRangesChecked (rangeOfDecls decls) mb - - /// Get information for implementation file + + /// Get information for implementation file let getNavigationFromImplFile (modules: SynModuleOrNamespace list) = // Map for dealing with name conflicts - let nameMap = ref Map.empty + let nameMap = ref Map.empty - let addItemName name = + let addItemName name = let count = defaultArg (!nameMap |> Map.tryFind name) 0 nameMap := (Map.add name (count + 1) (!nameMap)) (count + 1) - - let uniqueName name idx = + + let uniqueName name idx = let total = Map.find name (!nameMap) sprintf "%s_%d_of_%d" name idx total - // Create declaration (for the left dropdown) + // Create declaration (for the left dropdown) let createDeclLid(baseName, lid, kind, baseGlyph, m, bodym, nested, enclosingEntityKind, isAbstract, access) = let name = (if baseName <> "" then baseName + "." else "") + (textOfLid lid) - NavigationDeclarationItem.Create + NavigationItem.Create (name, kind, baseGlyph, m, bodym, false, enclosingEntityKind, isAbstract, access), (addItemName name), nested - + let createDecl(baseName, id:Ident, kind, baseGlyph, m, bodym, nested, enclosingEntityKind, isAbstract, access) = let name = (if baseName <> "" then baseName + "." else "") + (id.idText) - NavigationDeclarationItem.Create + NavigationItem.Create (name, kind, baseGlyph, m, bodym, false, enclosingEntityKind, isAbstract, access), (addItemName name), nested - + // Create member-kind-of-thing for the right dropdown let createMemberLid(lid, kind, baseGlyph, m, enclosingEntityKind, isAbstract, access) = - NavigationDeclarationItem.Create(textOfLid lid, kind, baseGlyph, m, m, false, enclosingEntityKind, isAbstract, access), (addItemName(textOfLid lid)) + NavigationItem.Create(textOfLid lid, kind, baseGlyph, m, m, false, enclosingEntityKind, isAbstract, access), (addItemName(textOfLid lid)) let createMember(id:Ident, kind, baseGlyph, m, enclosingEntityKind, isAbstract, access) = - NavigationDeclarationItem.Create(id.idText, kind, baseGlyph, m, m, false, enclosingEntityKind, isAbstract, access), (addItemName(id.idText)) + NavigationItem.Create(id.idText, kind, baseGlyph, m, m, false, enclosingEntityKind, isAbstract, access), (addItemName(id.idText)) // Process let-binding let processBinding isMember enclosingEntityKind isAbstract (Binding(_, _, _, _, _, _, SynValData(memebrOpt, _, _), synPat, _, synExpr, _, _)) = - let m = - match synExpr with + let m = + match synExpr with | SynExpr.Typed(e, _, _) -> e.Range // fix range for properties with type annotations | _ -> synExpr.Range match synPat, memebrOpt with - | SynPat.LongIdent(longDotId=LongIdentWithDots(lid,_); accessibility=access), Some(flags) when isMember -> + | SynPat.LongIdent(longDotId=LongIdentWithDots(lid,_); accessibility=access), Some(flags) when isMember -> let icon, kind = match flags.MemberKind with | MemberKind.ClassConstructor | MemberKind.Constructor - | MemberKind.Member -> + | MemberKind.Member -> (if flags.IsOverrideOrExplicitImpl then FSharpGlyph.OverridenMethod else FSharpGlyph.Method), MethodDecl | MemberKind.PropertyGetSet | MemberKind.PropertySet | MemberKind.PropertyGet -> FSharpGlyph.Property, PropertyDecl - let lidShow, rangeMerge = - match lid with - | _thisVar::nm::_ -> (List.tail lid, nm.idRange) - | hd::_ -> (lid, hd.idRange) + let lidShow, rangeMerge = + match lid with + | _thisVar::nm::_ -> (List.tail lid, nm.idRange) + | hd::_ -> (lid, hd.idRange) | _ -> (lid, m) [ createMemberLid(lidShow, kind, icon, unionRanges rangeMerge m, enclosingEntityKind, isAbstract, access) ] - | SynPat.LongIdent(LongIdentWithDots(lid,_), _, _, _, access, _), _ -> + | SynPat.LongIdent(LongIdentWithDots(lid,_), _, _, _, access, _), _ -> [ createMemberLid(lid, FieldDecl, FSharpGlyph.Field, unionRanges (List.head lid).idRange m, enclosingEntityKind, isAbstract, access) ] - | SynPat.Named(_, id, _, access, _), _ -> + | SynPat.Named(_, id, _, access, _), _ -> let glyph = if isMember then FSharpGlyph.Method else FSharpGlyph.Field [ createMember(id, FieldDecl, glyph, unionRanges id.idRange m, enclosingEntityKind, isAbstract, access) ] | _ -> [] - + // Process a class declaration or F# type declaration let rec processExnDefnRepr baseName nested (SynExceptionDefnRepr(_, (UnionCase(_, id, fldspec, _, _, _)), _, _, access, m)) = // Exception declaration - [ createDecl(baseName, id, ExnDecl, FSharpGlyph.Exception, m, fldspecRange fldspec, nested, FSharpEnclosingEntityKind.Exception, false, access) ] + [ createDecl(baseName, id, ExnDecl, FSharpGlyph.Exception, m, fldspecRange fldspec, nested, FSharpEnclosingEntityKind.Exception, false, access) ] // Process a class declaration or F# type declaration - and processExnDefn baseName (SynExceptionDefn(repr, membDefns, _)) = + and processExnDefn baseName (SynExceptionDefn(repr, membDefns, _)) = let nested = processMembers membDefns FSharpEnclosingEntityKind.Exception |> snd processExnDefnRepr baseName nested repr @@ -153,24 +158,24 @@ module Navigation = // F# class declaration let members = processMembers membDefns FSharpEnclosingEntityKind.Class |> snd let nested = members@topMembers - ([ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Class, m, bodyRange mb nested, nested, FSharpEnclosingEntityKind.Class, false, access) ]: ((NavigationDeclarationItem * int * _) list)) + ([ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Class, m, bodyRange mb nested, nested, FSharpEnclosingEntityKind.Class, false, access) ]: ((NavigationItem * int * _) list)) | SynTypeDefnRepr.Simple(simple, _) -> // F# type declaration match simple with | SynTypeDefnSimpleRepr.Union(_, cases, mb) -> - let cases = - [ for (UnionCase(_, id, fldspec, _, _, _)) in cases -> + let cases = + [ for (UnionCase(_, id, fldspec, _, _, _)) in cases -> createMember(id, OtherDecl, FSharpGlyph.Struct, unionRanges (fldspecRange fldspec) id.idRange, FSharpEnclosingEntityKind.DU, false, access) ] - let nested = cases@topMembers + let nested = cases@topMembers [ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Union, m, bodyRange mb nested, nested, FSharpEnclosingEntityKind.DU, false, access) ] - | SynTypeDefnSimpleRepr.Enum(cases, mb) -> - let cases = + | SynTypeDefnSimpleRepr.Enum(cases, mb) -> + let cases = [ for (EnumCase(_, id, _, _, m)) in cases -> createMember(id, FieldDecl, FSharpGlyph.EnumMember, m, FSharpEnclosingEntityKind.Enum, false, access) ] let nested = cases@topMembers [ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Enum, m, bodyRange mb nested, nested, FSharpEnclosingEntityKind.Enum, false, access) ] | SynTypeDefnSimpleRepr.Record(_, fields, mb) -> - let fields = + let fields = [ for (Field(_, _, id, _, _, _, _, m)) in fields do if (id.IsSome) then yield createMember(id.Value, FieldDecl, FSharpGlyph.Field, m, FSharpEnclosingEntityKind.Record, false, access) ] @@ -178,16 +183,16 @@ module Navigation = [ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Type, m, bodyRange mb nested, nested, FSharpEnclosingEntityKind.Record, false, access) ] | SynTypeDefnSimpleRepr.TypeAbbrev(_, _, mb) -> [ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Typedef, m, bodyRange mb topMembers, topMembers, FSharpEnclosingEntityKind.Class, false, access) ] - - //| SynTypeDefnSimpleRepr.General of TyconKind * (SynType * range * ident option) list * (valSpfn * MemberFlags) list * fieldDecls * bool * bool * range + + //| SynTypeDefnSimpleRepr.General of TyconKind * (SynType * range * ident option) list * (valSpfn * MemberFlags) list * fieldDecls * bool * bool * range //| SynTypeDefnSimpleRepr.LibraryOnlyILAssembly of ILType * range //| TyconCore_repr_hidden of range - | _ -> [] - - // Returns class-members for the right dropdown - and processMembers members enclosingEntityKind : (range * list) = - let members = - members + | _ -> [] + + // Returns class-members for the right dropdown + and processMembers members enclosingEntityKind : (Text.range * list) = + let members = + members |> List.groupBy (fun x -> x.Range) |> List.map (fun (range, members) -> range, @@ -198,14 +203,14 @@ module Navigation = | SynMemberDefn.Member(bind, _) -> processBinding true enclosingEntityKind false bind | SynMemberDefn.ValField(Field(_, _, Some(rcid), ty, _, _, access, _), _) -> [ createMember(rcid, FieldDecl, FSharpGlyph.Field, ty.Range, enclosingEntityKind, false, access) ] - | SynMemberDefn.AutoProperty(_attribs,_isStatic,id,_tyOpt,_propKind,_,_xmlDoc, access,_synExpr, _, _) -> + | SynMemberDefn.AutoProperty(_attribs,_isStatic,id,_tyOpt,_propKind,_,_xmlDoc, access,_synExpr, _, _) -> [ createMember(id, FieldDecl, FSharpGlyph.Field, id.idRange, enclosingEntityKind, false, access) ] | SynMemberDefn.AbstractSlot(ValSpfn(_, id, _, ty, _, _, _, _, access, _, _), _, _) -> [ createMember(id, MethodDecl, FSharpGlyph.OverridenMethod, ty.Range, enclosingEntityKind, true, access) ] - | SynMemberDefn.NestedType _ -> failwith "tycon as member????" //processTycon tycon + | SynMemberDefn.NestedType _ -> failwith "tycon as member????" //processTycon tycon | SynMemberDefn.Interface(_, Some(membs), _) -> processMembers membs enclosingEntityKind |> snd - | _ -> [] + | _ -> [] // can happen if one is a getter and one is a setter | [SynMemberDefn.Member(memberDefn=Binding(headPat=SynPat.LongIdent(lid1, Some(info1),_,_,_,_)) as binding1) SynMemberDefn.Member(memberDefn=Binding(headPat=SynPat.LongIdent(lid2, Some(info2),_,_,_,_)) as binding2)] -> @@ -218,42 +223,42 @@ module Navigation = match processBinding true enclosingEntityKind false binding1 with | [] -> processBinding true enclosingEntityKind false binding2 | x -> x - | _ -> [])) - - (members |> Seq.map fst |> Seq.fold unionRangesChecked range.Zero), + | _ -> [])) + + (members |> Seq.map fst |> Seq.fold unionRangesChecked Text.range.Zero), (members |> List.map snd |> List.concat) // Process declarations in a module that belong to the right drop-down (let bindings) let processNestedDeclarations decls = decls |> List.collect (function | SynModuleDecl.Let(_, binds, _) -> List.collect (processBinding false FSharpEnclosingEntityKind.Module false) binds - | _ -> []) + | _ -> []) // Process declarations nested in a module that should be displayed in the left dropdown - // (such as type declarations, nested modules etc.) + // (such as type declarations, nested modules etc.) let rec processFSharpNavigationTopLevelDeclarations(baseName, decls) = decls |> List.collect (function | SynModuleDecl.ModuleAbbrev(id, lid, m) -> [ createDecl(baseName, id, ModuleDecl, FSharpGlyph.Module, m, rangeOfLid lid, [], FSharpEnclosingEntityKind.Namespace, false, None) ] - + | SynModuleDecl.NestedModule(ComponentInfo(_, _, _, lid, _, _, access, _), _isRec, decls, _, m) -> // Find let bindings (for the right dropdown) let nested = processNestedDeclarations(decls) let newBaseName = (if (baseName = "") then "" else baseName+".") + (textOfLid lid) - + // Get nested modules and types (for the left dropdown) let other = processFSharpNavigationTopLevelDeclarations(newBaseName, decls) createDeclLid(baseName, lid, ModuleDecl, FSharpGlyph.Module, m, unionRangesChecked (rangeOfDecls nested) (moduleRange (rangeOfLid lid) other), nested, FSharpEnclosingEntityKind.Module, false, access) :: other - - | SynModuleDecl.Types(tydefs, _) -> tydefs |> List.collect (processTycon baseName) + + | SynModuleDecl.Types(tydefs, _) -> tydefs |> List.collect (processTycon baseName) | SynModuleDecl.Exception (defn,_) -> processExnDefn baseName defn | _ -> []) - // Collect all the items - let items = + // Collect all the items + let items = // Show base name for this module only if it's not the root one let singleTopLevel = (modules.Length = 1) modules |> List.collect (fun (SynModuleOrNamespace(id, _isRec, kind, decls, _, _, access, m)) -> - let isModule = - match kind with + let isModule = + match kind with | SynModuleOrNamespaceKind.AnonModule | SynModuleOrNamespaceKind.NamedModule -> true | _ -> false let baseName = if (not singleTopLevel) then textOfLid id else "" @@ -267,54 +272,54 @@ module Navigation = | [] -> other | _ -> let decl = - NavigationDeclarationItem.Create + NavigationItem.Create (textOfLid id, (if isModule then ModuleFileDecl else NamespaceDecl), - FSharpGlyph.Module, m, - unionRangesChecked (rangeOfDecls nested) (moduleRange (rangeOfLid id) other), + FSharpGlyph.Module, m, + unionRangesChecked (rangeOfDecls nested) (moduleRange (rangeOfLid id) other), singleTopLevel, FSharpEnclosingEntityKind.Module, false, access), (addItemName(textOfLid id)), nested decl::other) - - let items = - items - |> Array.ofList - |> Array.map (fun (d, idx, nest) -> + + let items = + items + |> Array.ofList + |> Array.map (fun (d, idx, nest) -> let nest = nest |> Array.ofList |> Array.map (fun (decl, idx) -> decl.WithUniqueName(uniqueName d.Name idx)) nest |> Array.sortInPlaceWith (fun a b -> compare a.Name b.Name) - { Declaration = d.WithUniqueName(uniqueName d.Name idx); Nested = nest } ) + { Declaration = d.WithUniqueName(uniqueName d.Name idx); Nested = nest } ) items |> Array.sortInPlaceWith (fun a b -> compare a.Declaration.Name b.Declaration.Name) new FSharpNavigationItems(items) - /// Get information for signature file + /// Get information for signature file let getNavigationFromSigFile (modules: SynModuleOrNamespaceSig list) = // Map for dealing with name conflicts - let nameMap = ref Map.empty - let addItemName name = + let nameMap = ref Map.empty + let addItemName name = let count = defaultArg (!nameMap |> Map.tryFind name) 0 nameMap := (Map.add name (count + 1) (!nameMap)) (count + 1) - let uniqueName name idx = + let uniqueName name idx = let total = Map.find name (!nameMap) sprintf "%s_%d_of_%d" name idx total - // Create declaration (for the left dropdown) + // Create declaration (for the left dropdown) let createDeclLid(baseName, lid, kind, baseGlyph, m, bodym, nested, enclosingEntityKind, isAbstract, access) = let name = (if baseName <> "" then baseName + "." else "") + (textOfLid lid) - NavigationDeclarationItem.Create + NavigationItem.Create (name, kind, baseGlyph, m, bodym, false, enclosingEntityKind, isAbstract, access), (addItemName name), nested - + let createDecl(baseName, id:Ident, kind, baseGlyph, m, bodym, nested, enclosingEntityKind, isAbstract, access) = let name = (if baseName <> "" then baseName + "." else "") + (id.idText) - NavigationDeclarationItem.Create + NavigationItem.Create (name, kind, baseGlyph, m, bodym, false, enclosingEntityKind, isAbstract, access), (addItemName name), nested - + let createMember(id:Ident, kind, baseGlyph, m, enclosingEntityKind, isAbstract, access) = - NavigationDeclarationItem.Create(id.idText, kind, baseGlyph, m, m, false, enclosingEntityKind, isAbstract, access), (addItemName(id.idText)) + NavigationItem.Create(id.idText, kind, baseGlyph, m, m, false, enclosingEntityKind, isAbstract, access), (addItemName(id.idText)) let rec processExnRepr baseName nested (SynExceptionDefnRepr(_, (UnionCase(_, id, fldspec, _, _, _)), _, _, access, m)) = // Exception declaration - [ createDecl(baseName, id, ExnDecl, FSharpGlyph.Exception, m, fldspecRange fldspec, nested, FSharpEnclosingEntityKind.Exception, false, access) ] - - and processExnSig baseName (SynExceptionSig(repr, memberSigs, _)) = + [ createDecl(baseName, id, ExnDecl, FSharpGlyph.Exception, m, fldspecRange fldspec, nested, FSharpEnclosingEntityKind.Exception, false, access) ] + + and processExnSig baseName (SynExceptionSig(repr, memberSigs, _)) = let nested = processSigMembers memberSigs processExnRepr baseName nested repr @@ -326,24 +331,24 @@ module Navigation = // F# class declaration let members = processSigMembers membDefns let nested = members @ topMembers - ([ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Class, m, bodyRange mb nested, nested, FSharpEnclosingEntityKind.Class, false, access) ]: ((NavigationDeclarationItem * int * _) list)) + ([ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Class, m, bodyRange mb nested, nested, FSharpEnclosingEntityKind.Class, false, access) ]: ((NavigationItem * int * _) list)) | SynTypeDefnSigRepr.Simple(simple, _) -> // F# type declaration match simple with | SynTypeDefnSimpleRepr.Union(_, cases, mb) -> - let cases = - [ for (UnionCase(_, id, fldspec, _, _, _)) in cases -> + let cases = + [ for (UnionCase(_, id, fldspec, _, _, _)) in cases -> createMember(id, OtherDecl, FSharpGlyph.Struct, unionRanges (fldspecRange fldspec) id.idRange, FSharpEnclosingEntityKind.DU, false, access) ] - let nested = cases@topMembers + let nested = cases@topMembers [ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Union, m, bodyRange mb nested, nested, FSharpEnclosingEntityKind.DU, false, access) ] - | SynTypeDefnSimpleRepr.Enum(cases, mb) -> - let cases = + | SynTypeDefnSimpleRepr.Enum(cases, mb) -> + let cases = [ for (EnumCase(_, id, _, _, m)) in cases -> createMember(id, FieldDecl, FSharpGlyph.EnumMember, m, FSharpEnclosingEntityKind.Enum, false, access) ] let nested = cases@topMembers [ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Enum, m, bodyRange mb nested, nested, FSharpEnclosingEntityKind.Enum, false, access) ] | SynTypeDefnSimpleRepr.Record(_, fields, mb) -> - let fields = + let fields = [ for (Field(_, _, id, _, _, _, _, m)) in fields do if (id.IsSome) then yield createMember(id.Value, FieldDecl, FSharpGlyph.Field, m, FSharpEnclosingEntityKind.Record, false, access) ] @@ -351,13 +356,13 @@ module Navigation = [ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Type, m, bodyRange mb nested, nested, FSharpEnclosingEntityKind.Record, false, access) ] | SynTypeDefnSimpleRepr.TypeAbbrev(_, _, mb) -> [ createDeclLid(baseName, lid, TypeDecl, FSharpGlyph.Typedef, m, bodyRange mb topMembers, topMembers, FSharpEnclosingEntityKind.Class, false, access) ] - - //| SynTypeDefnSimpleRepr.General of TyconKind * (SynType * range * ident option) list * (valSpfn * MemberFlags) list * fieldDecls * bool * bool * range + + //| SynTypeDefnSimpleRepr.General of TyconKind * (SynType * range * ident option) list * (valSpfn * MemberFlags) list * fieldDecls * bool * bool * range //| SynTypeDefnSimpleRepr.LibraryOnlyILAssembly of ILType * range //| TyconCore_repr_hidden of range - | _ -> [] - - and processSigMembers (members: SynMemberSig list): list = + | _ -> [] + + and processSigMembers (members: SynMemberSig list): list = [ for memb in members do match memb with | SynMemberSig.Member(SynValSig.ValSpfn(_, id, _, _, _, _, _, _, access, _, m), _, _) -> @@ -370,34 +375,34 @@ module Navigation = let processNestedSigDeclarations decls = decls |> List.collect (function | SynModuleSigDecl.Val(SynValSig.ValSpfn(_, id, _, _, _, _, _, _, access, _, m), _) -> [ createMember(id, MethodDecl, FSharpGlyph.Method, m, FSharpEnclosingEntityKind.Module, false, access) ] - | _ -> [] ) + | _ -> [] ) // Process declarations nested in a module that should be displayed in the left dropdown - // (such as type declarations, nested modules etc.) + // (such as type declarations, nested modules etc.) let rec processFSharpNavigationTopLevelSigDeclarations(baseName, decls) = decls |> List.collect (function | SynModuleSigDecl.ModuleAbbrev(id, lid, m) -> [ createDecl(baseName, id, ModuleDecl, FSharpGlyph.Module, m, rangeOfLid lid, [], FSharpEnclosingEntityKind.Module, false, None) ] - - | SynModuleSigDecl.NestedModule(ComponentInfo(_, _, _, lid, _, _, access, _), _, decls, m) -> + + | SynModuleSigDecl.NestedModule(ComponentInfo(_, _, _, lid, _, _, access, _), _, decls, m) -> // Find let bindings (for the right dropdown) let nested = processNestedSigDeclarations(decls) let newBaseName = (if (baseName = "") then "" else baseName+".") + (textOfLid lid) - + // Get nested modules and types (for the left dropdown) let other = processFSharpNavigationTopLevelSigDeclarations(newBaseName, decls) createDeclLid(baseName, lid, ModuleDecl, FSharpGlyph.Module, m, unionRangesChecked (rangeOfDecls nested) (moduleRange (rangeOfLid lid) other), nested, FSharpEnclosingEntityKind.Module, false, access)::other - - | SynModuleSigDecl.Types(tydefs, _) -> tydefs |> List.collect (processTycon baseName) + + | SynModuleSigDecl.Types(tydefs, _) -> tydefs |> List.collect (processTycon baseName) | SynModuleSigDecl.Exception (defn,_) -> processExnSig baseName defn | _ -> []) - - // Collect all the items - let items = + + // Collect all the items + let items = // Show base name for this module only if it's not the root one let singleTopLevel = (modules.Length = 1) modules |> List.collect (fun (SynModuleOrNamespaceSig(id, _isRec, kind, decls, _, _, access, m)) -> - let isModule = - match kind with + let isModule = + match kind with | SynModuleOrNamespaceKind.AnonModule | SynModuleOrNamespaceKind.NamedModule -> true | _ -> false let baseName = if (not singleTopLevel) then textOfLid id else "" @@ -405,24 +410,24 @@ module Navigation = let nested = processNestedSigDeclarations(decls) // Get nested modules and types (for the left dropdown) let other = processFSharpNavigationTopLevelSigDeclarations(baseName, decls) - + // Create explicitly - it can be 'single top level' thing that is hidden let decl = - NavigationDeclarationItem.Create + NavigationItem.Create (textOfLid id, (if isModule then ModuleFileDecl else NamespaceDecl), - FSharpGlyph.Module, m, - unionRangesChecked (rangeOfDecls nested) (moduleRange (rangeOfLid id) other), + FSharpGlyph.Module, m, + unionRangesChecked (rangeOfDecls nested) (moduleRange (rangeOfLid id) other), singleTopLevel, FSharpEnclosingEntityKind.Module, false, access), (addItemName(textOfLid id)), nested decl::other) - - let items = - items - |> Array.ofList - |> Array.map (fun (d, idx, nest) -> + + let items = + items + |> Array.ofList + |> Array.map (fun (d, idx, nest) -> let nest = nest |> Array.ofList |> Array.map (fun (decl, idx) -> decl.WithUniqueName(uniqueName d.Name idx)) nest |> Array.sortInPlaceWith (fun a b -> compare a.Name b.Name) - let nest = nest |> Array.distinctBy (fun x -> x.Range, x.BodyRange, x.Name, x.Kind) - - { Declaration = d.WithUniqueName(uniqueName d.Name idx); Nested = nest } ) + let nest = nest |> Array.distinctBy (fun x -> x.Range, x.BodyRange, x.Name, x.Kind) + + { Declaration = d.WithUniqueName(uniqueName d.Name idx); Nested = nest } ) items |> Array.sortInPlaceWith (fun a b -> compare a.Declaration.Name b.Declaration.Name) - new FSharpNavigationItems(items) \ No newline at end of file + new FSharpNavigationItems(items) diff --git a/src/FSharpLanguageServer/Program.fs b/src/FSharpLanguageServer/Program.fs index 90a2a76..e95e349 100644 --- a/src/FSharpLanguageServer/Program.fs +++ b/src/FSharpLanguageServer/Program.fs @@ -14,37 +14,35 @@ open FSharp.Data open FSharp.Data.JsonExtensions open Conversions -module Ast = FSharp.Compiler.Ast - let private TODO() = raise (Exception "TODO") /// Look for a method call like foo.MyMethod() before the cursor /// (exposed for testing) -let findMethodCallBeforeCursor(lineContent: string, cursor: int): int option = +let findMethodCallBeforeCursor(lineContent: string, cursor: int): int option = let mutable found = -1 let mutable parenDepth = 0 - for i in (min (cursor-1) lineContent.Length) .. -1 .. 0 do - match lineContent.[i] with + for i in (min (cursor-1) lineContent.Length) .. -1 .. 0 do + match lineContent.[i] with | ')' -> parenDepth <- parenDepth + 1 | '(' when parenDepth > 0 -> parenDepth <- parenDepth - 1 | '(' when found = -1 -> found <- i | _ -> () - if found = -1 then None - else + if found = -1 then None + else let prefix = lineContent.Substring(0, found).TrimEnd() - if Regex(@"let[ \w]+$").IsMatch(prefix) then - dprintfn "No signature help in let expression %s" lineContent - None - else if Regex(@"member[ \w\.]+$").IsMatch(prefix) then - dprintfn "No signature help in member expression %s" lineContent - None + if Regex(@"let[ \w]+$").IsMatch(prefix) then + dprintfn "No signature help in let expression %s" lineContent + None + else if Regex(@"member[ \w\.]+$").IsMatch(prefix) then + dprintfn "No signature help in member expression %s" lineContent + None else Some prefix.Length /// Figure out the active parameter by counting ',' characters -let private countCommas(lineContent: string, endOfMethodName: int, cursor: int): int = +let private countCommas(lineContent: string, endOfMethodName: int, cursor: int): int = let mutable count = 0 - for i in endOfMethodName .. (min (cursor-1) lineContent.Length) do - if lineContent.[i] = ',' then + for i in endOfMethodName .. (min (cursor-1) lineContent.Length) do + if lineContent.[i] = ',' then count <- count + 1 count @@ -52,74 +50,74 @@ let private countCommas(lineContent: string, endOfMethodName: int, cursor: int): /// Matches can be discontinuous if the letters of `find` match the first letters of words in `candidate` /// For example, fb matches FooBar, but it doesn't match Foobar /// (exposed for testing) -let matchesTitleCase(find: string, candidate: string): bool = +let matchesTitleCase(find: string, candidate: string): bool = let mutable i = 0 - let lowerEquals(x, y) = + let lowerEquals(x, y) = Char.ToLower(x) = Char.ToLower(y) - let matchNextChar(f) = - if i < candidate.Length && lowerEquals(candidate.[i], f) then - i <- i + 1 - true - else false - let isStartOfWord(i) = + let matchNextChar(f) = + if i < candidate.Length && lowerEquals(candidate.[i], f) then + i <- i + 1 + true + else false + let isStartOfWord(i) = 0 <= i && i < candidate.Length && Char.IsUpper(candidate.[i]) - let matchStartOfNextWord(f) = + let matchStartOfNextWord(f) = let test(i) = isStartOfWord(i) && lowerEquals(candidate.[i], f) while i < candidate.Length && not(test(i)) do - i <- i + 1 + i <- i + 1 test(i) let mutable matched = true - for f in find do + for f in find do matched <- matched && (matchNextChar(f) || matchStartOfNextWord(f)) matched /// Check if an F# symbol matches a query typed by the user -let private matchesQuery(query: string, candidate: string): bool = +let private matchesQuery(query: string, candidate: string): bool = matchesTitleCase(query, candidate) /// Find the first overload in `method` that is compatible with `activeParameter` // TODO actually consider types -let private findCompatibleOverload(activeParameter: int, methods: FSharpMethodGroupItem[]): int option = - let mutable result = -1 - for i in 0 .. methods.Length - 1 do - if result = -1 && (activeParameter = 0 || activeParameter < methods.[i].Parameters.Length) then - result <- i +let private findCompatibleOverload(activeParameter: int, methods: FSharpMethodGroupItem[]): int option = + let mutable result = -1 + for i in 0 .. methods.Length - 1 do + if result = -1 && (activeParameter = 0 || activeParameter < methods.[i].Parameters.Length) then + result <- i if result = -1 then None else Some result /// Find searchable declarations -let private findDeclarations(parse: FSharpParseFileResults) = - let items = - match parse.ParseTree with - | Some(Ast.ParsedInput.SigFile(Ast.ParsedSigFileInput(_, _, _, _, modules))) -> +let private findDeclarations(parse: FSharpParseFileResults) = + let items = + match parse.ParseTree with + | Some(SyntaxTree.ParsedInput.SigFile(SyntaxTree.ParsedSigFileInput(_, _, _, _, modules))) -> Navigation.getNavigationFromSigFile(modules).Declarations - | Some(Ast.ParsedInput.ImplFile(Ast.ParsedImplFileInput(_, _, _, _, _, modules, _))) -> + | Some(SyntaxTree.ParsedInput.ImplFile(SyntaxTree.ParsedImplFileInput(_, _, _, _, _, modules, _))) -> Navigation.getNavigationFromImplFile(modules).Declarations | _ -> [||] - [ for i in items do + [ for i in items do yield i.Declaration, None - for n in i.Nested do + for n in i.Nested do yield n, Some(i.Declaration) ] -let private findSignatureDeclarations(parse: FSharpParseFileResults) = - match parse.ParseTree with - | Some(Ast.ParsedInput.SigFile(Ast.ParsedSigFileInput(_, _, _, _, modules))) -> +let private findSignatureDeclarations(parse: FSharpParseFileResults) = + match parse.ParseTree with + | Some(SyntaxTree.ParsedInput.SigFile(SyntaxTree.ParsedSigFileInput(_, _, _, _, modules))) -> let items = Navigation.getNavigationFromSigFile(modules) - [ for i in items.Declarations do - for n in i.Nested do + [ for i in items.Declarations do + for n in i.Nested do yield [i.Declaration.Name; n.Name], n.Range ] | _ -> [] -let private findSignatureImplementation(parse: FSharpParseFileResults, name: string list) = - match parse.ParseTree with - | Some(Ast.ParsedInput.ImplFile(Ast.ParsedImplFileInput(_, _, _, _, _, modules, _))) -> +let private findSignatureImplementation(parse: FSharpParseFileResults, name: string list) = + match parse.ParseTree with + | Some(SyntaxTree.ParsedInput.ImplFile(SyntaxTree.ParsedImplFileInput(_, _, _, _, _, modules, _))) -> let items = Navigation.getNavigationFromImplFile(modules) - [ for i in items.Declarations do - for n in i.Nested do + [ for i in items.Declarations do + for n in i.Nested do if [i.Declaration.Name; n.Name] = name then yield n.Range ] | _ -> [] /// Find functions annotated with [] -let private testFunctions(parse: FSharpParseFileResults): (string list * Ast.SynBinding) list = +let private testFunctions(parse: FSharpParseFileResults): (string list * SyntaxTree.SynBinding) list = let (|XunitTest|_|) str = match str with | "Fact" | "Xunit.FactAttribute" @@ -129,108 +127,108 @@ let private testFunctions(parse: FSharpParseFileResults): (string list * Ast.Syn match str with | "Test" | "NUnit.Framework.Test" -> Some true | _ -> None - let isTestAttribute(a: Ast.SynAttribute): bool = + let isTestAttribute(a: SyntaxTree.SynAttribute): bool = let ids = a.TypeName.Lid let string = String.concat "." [for i in ids do yield i.idText] - match string with + match string with // TODO check for open NUnit.Framework before accepting plain "Test" | NUnitTest _ | XunitTest _ -> true | _ -> false - let isTestFunction(binding: Ast.SynBinding): bool = - let attrs = match binding with Ast.Binding(_, _, _, _, attrs, _, _, _, _, _, _, _) -> attrs + let isTestFunction(binding: SyntaxTree.SynBinding): bool = + let attrs = match binding with SyntaxTree.Binding(_, _, _, _, attrs, _, _, _, _, _, _, _) -> attrs let mutable found = false - for list in attrs do - for a in list.Attributes do - if isTestAttribute(a) then + for list in attrs do + for a in list.Attributes do + if isTestAttribute(a) then found <- true found - let name(binding: Ast.SynBinding): string list = - match binding with - | Ast.Binding(_, _, _, _, _, _, _, Ast.SynPat.LongIdent(Ast.LongIdentWithDots(ids, _), _, _, _, _, _), _, _, _, _) -> + let name(binding: SyntaxTree.SynBinding): string list = + match binding with + | SyntaxTree.Binding(_, _, _, _, _, _, _, SyntaxTree.SynPat.LongIdent(SyntaxTree.LongIdentWithDots(ids, _), _, _, _, _, _), _, _, _, _) -> [for i in ids do yield i.idText] | _ -> [] - let rec bindings(ctx: string list, m: Ast.SynModuleDecl): (string list * Ast.SynBinding) seq = + let rec bindings(ctx: string list, m: SyntaxTree.SynModuleDecl): (string list * SyntaxTree.SynBinding) seq = seq { - match m with - | Ast.SynModuleDecl.NestedModule(outer, _, decls, _, _) -> - let ids = match outer with Ast.ComponentInfo(_, _, _, ids, _, _, _, _) -> ids + match m with + | SyntaxTree.SynModuleDecl.NestedModule(outer, _, decls, _, _) -> + let ids = match outer with SyntaxTree.ComponentInfo(_, _, _, ids, _, _, _, _) -> ids let ctx = ctx@[for i in ids do yield i.idText] - for d in decls do + for d in decls do yield! bindings(ctx, d) - | Ast.SynModuleDecl.Let(_, bindings, _) -> - for b in bindings do + | SyntaxTree.SynModuleDecl.Let(_, bindings, _) -> + for b in bindings do yield ctx@name(b), b - | Ast.SynModuleDecl.Types(defs, _) -> - for d in defs do - match d with - | Ast.TypeDefn(Ast.ComponentInfo(_, _, _, ids, _, _, _, _), _, members, _) -> + | SyntaxTree.SynModuleDecl.Types(defs, _) -> + for d in defs do + match d with + | SyntaxTree.TypeDefn(SyntaxTree.ComponentInfo(_, _, _, ids, _, _, _, _), _, members, _) -> let ctx = ctx@[for i in ids do yield i.idText] - for m in members do - match m with - | Ast.SynMemberDefn.Member(b, _) -> + for m in members do + match m with + | SyntaxTree.SynMemberDefn.Member(b, _) -> yield ctx@name(b), b - | Ast.SynMemberDefn.LetBindings(bindings, _, _, _) -> - for b in bindings do + | SyntaxTree.SynMemberDefn.LetBindings(bindings, _, _, _) -> + for b in bindings do yield ctx@name(b), b | _ -> () | _ -> () } - let modules = - match parse.ParseTree with - | Some(Ast.ParsedInput.ImplFile(Ast.ParsedImplFileInput(_, _, _, _, _, modules, _))) -> modules + let modules = + match parse.ParseTree with + | Some(SyntaxTree.ParsedInput.ImplFile(SyntaxTree.ParsedImplFileInput(_, _, _, _, _, modules, _))) -> modules | _ -> [] - [ for m in modules do - let ids, decls = match m with Ast.SynModuleOrNamespace(ids, _, _, decls, _, _, _, _) -> ids, decls + [ for m in modules do + let ids, decls = match m with SyntaxTree.SynModuleOrNamespace(ids, _, _, decls, _, _, _, _) -> ids, decls let name = [for i in ids do yield i.idText] - for d in decls do - for ctx, b in bindings(name, d) do - if isTestFunction(b) then + for d in decls do + for ctx, b in bindings(name, d) do + if isTestFunction(b) then yield ctx, b ] -type Server(client: ILanguageClient) = +type Server(client: ILanguageClient) = let docs = DocumentStore() let checker = FSharpChecker.Create() let projects = ProjectManager(checker) /// Get a file from docs, or read it from disk - let getOrRead(file: FileInfo): string option = - match docs.GetText(file) with + let getOrRead(file: FileInfo): string option = + match docs.GetText(file) with | Some(text) -> Some(text) | None when file.Exists -> Some(File.ReadAllText(file.FullName)) | None -> None /// Read a specific line from a file - let lineContent(file: FileInfo, targetLine: int): string = + let lineContent(file: FileInfo, targetLine: int): string = let text = getOrRead(file) |> Option.defaultValue "" let reader = new StringReader(text) let mutable line = 0 - while line < targetLine && reader.Peek() <> -1 do + while line < targetLine && reader.Peek() <> -1 do reader.ReadLine() |> ignore line <- line + 1 - if reader.Peek() = -1 then + if reader.Peek() = -1 then dprintfn "Reached EOF before line %d in file %O" targetLine file.Name - "" - else + "" + else reader.ReadLine() - /// Parse a file - let parseFile(file: FileInfo): Async> = + /// Parse a file + let parseFile(file: FileInfo): Async> = async { - match projects.FindProjectOptions(file), getOrRead(file) with + match projects.FindProjectOptions(file), getOrRead(file) with | Error(_), _ -> return Error(sprintf "Can't find symbols in %s because of error in project options" file.Name) - | _, None -> + | _, None -> return Error(sprintf "%s was closed" file.FullName) - | Ok(projectOptions), Some(sourceText) -> - match checker.TryGetRecentCheckResultsForFile(file.FullName, projectOptions) with - | Some(parse, _, _) -> + | Ok(projectOptions), Some(sourceText) -> + match checker.TryGetRecentCheckResultsForFile(file.FullName, projectOptions) with + | Some(parse, _, _) -> return Ok parse | None -> try let parsingOptions, _ = checker.GetParsingOptionsFromProjectOptions(projectOptions) let! parse = checker.ParseFile(file.FullName, SourceText.ofString(sourceText), parsingOptions) return Ok(parse) - with e -> + with e -> return Error(e.Message) } @@ -239,63 +237,63 @@ type Server(client: ILanguageClient) = /// `allowCached` is a bad idea if upstream dependencies of `file` have been edited /// If `allowStale`, will re-use stale results where the source doesn't match /// `allowStale` only really works for simple identifier expressions like x.y - let checkOpenFile(file: FileInfo, allowCached: bool, allowStale: bool): Async> = + let checkOpenFile(file: FileInfo, allowCached: bool, allowStale: bool): Async> = async { - match projects.FindProjectOptions(file), docs.Get(file) with - | _, None -> + match projects.FindProjectOptions(file), docs.Get(file) with + | _, None -> // If file doesn't exist, there's nothing to report dprintfn "%s was closed" file.FullName return Error [] - | Error(errs), _ -> + | Error(errs), _ -> return Error(errs) - | Ok(projectOptions), Some(sourceText, sourceVersion) -> + | Ok(projectOptions), Some(sourceText, sourceVersion) -> let recompile = async { let timeCheck = Stopwatch.StartNew() let! force = checker.ParseAndCheckFileInProject(file.FullName, sourceVersion, SourceText.ofString(sourceText), projectOptions) dprintfn "Checked %s in %dms" file.Name timeCheck.ElapsedMilliseconds - match force with + match force with | parseResult, FSharpCheckFileAnswer.Aborted -> return Error(asDiagnostics parseResult.Errors) | parseResult, FSharpCheckFileAnswer.Succeeded(checkResult) -> return Ok(parseResult, checkResult) } - match checker.TryGetRecentCheckResultsForFile(file.FullName, projectOptions) with - | Some(parseResult, checkResult, version) -> - if allowCached && version = sourceVersion then + match checker.TryGetRecentCheckResultsForFile(file.FullName, projectOptions) with + | Some(parseResult, checkResult, version) -> + if allowCached && version = sourceVersion then return Ok(parseResult, checkResult) - else if allowCached && allowStale then - try + else if allowCached && allowStale then + try dprintfn "Trying to recompile %s with timeout" file.Name let! worker = Async.StartChild(recompile, millisecondsTimeout=200) return! worker with :? TimeoutException -> dprintfn "Re-compile timed out, using stale results" return Ok(parseResult, checkResult) - else + else return! recompile - | _ -> + | _ -> return! recompile } /// When did we last check each file on disk? let lastCheckedOnDisk = new System.Collections.Generic.Dictionary() // TODO there might be a thread safety issue here---is this getting called from a separate thread? - do checker.BeforeBackgroundFileCheck.Add(fun(fileName, _) -> + do checker.BeforeBackgroundFileCheck.Add(fun(fileName, _) -> let file = FileInfo(fileName) lastCheckedOnDisk.[file.FullName] <- file.LastWriteTime) /// Figure out what files will be implicitly recompiled if we recompile `goal` - let needsRecompile(goal: FileInfo): List = - match projects.FindProjectOptions(goal) with - | Ok(projectOptions) -> + let needsRecompile(goal: FileInfo): List = + match projects.FindProjectOptions(goal) with + | Ok(projectOptions) -> // Find all projects that goal depends on, including its own project let projects = projects.TransitiveDeps(FileInfo(projectOptions.ProjectFileName)) // Take all files that lead up to goal, not including itself - let files = [for p in projects do + let files = [for p in projects do let sourceFiles = Array.map FileInfo p.SourceFiles let notGoal(f: FileInfo) = f.FullName <> goal.FullName yield! Array.takeWhile notGoal sourceFiles] // Skip files until we find a modified file, then take all remaining files - let notModified(f: FileInfo) = - match lastCheckedOnDisk.TryGetValue(f.FullName) with + let notModified(f: FileInfo) = + match lastCheckedOnDisk.TryGetValue(f.FullName) with | true, lastChecked -> f.LastWriteTime <= lastChecked | _, _ -> false let modified = List.skipWhile notModified files @@ -303,15 +301,15 @@ type Server(client: ILanguageClient) = modified@[goal] | Error(_) -> [] /// Send diagnostics to the client - let publishErrors(file: FileInfo, errors: Diagnostic list) = + let publishErrors(file: FileInfo, errors: Diagnostic list) = client.PublishDiagnostics({uri=Uri("file://" + file.FullName); diagnostics=errors}) /// Check a file - let getErrors(file: FileInfo, check: Result): Async = + let getErrors(file: FileInfo, check: Result): Async = async { match check with | Error(errors) -> return errors - | Ok(parseResult, checkResult) -> + | Ok(parseResult, checkResult) -> let parseErrors = asDiagnostics(parseResult.Errors) let typeErrors = asDiagnostics(checkResult.Errors) // This is just too slow. Also, it's sometimes wrong. @@ -322,15 +320,15 @@ type Server(client: ILanguageClient) = // dprintfn "Found %d unused opens in %dms" unusedOpenErrors.Length timeUnusedOpens.ElapsedMilliseconds // Find unused declarations let timeUnusedDeclarations = Stopwatch.StartNew() - let! uses = checkResult.GetAllUsesOfAllSymbolsInFile() - let unusedDeclarationRanges = UnusedDeclarations.getUnusedDeclarationRanges(uses, file.Name.EndsWith(".fsx")) + let uses = checkResult.GetAllUsesOfAllSymbolsInFile() + let unusedDeclarationRanges = UnusedDeclarations.getUnusedDeclarationRanges(Seq.toArray uses, file.Name.EndsWith(".fsx")) let unusedDeclarationErrors = [for r in unusedDeclarationRanges do yield diagnostic("Unused declaration", r, DiagnosticSeverity.Hint)] dprintfn "Found %d unused declarations in %dms" unusedDeclarationErrors.Length timeUnusedDeclarations.ElapsedMilliseconds // Combine // return parseErrors@typeErrors@unusedOpenErrors@unusedDeclarationErrors return parseErrors@typeErrors@unusedDeclarationErrors } - let doCheck(file: FileInfo): Async = + let doCheck(file: FileInfo): Async = async { let! check = checkOpenFile(file, true, false) let! errors = getErrors(file, check) @@ -339,23 +337,23 @@ type Server(client: ILanguageClient) = /// Request that `uri` be checked when the user stops doing things for 1 second let backgroundCheck = DebounceCheck(doCheck, 1000) /// Find the symbol at a position - let symbolAt(textDocument: TextDocumentIdentifier, position: Position): Async = + let symbolAt(textDocument: TextDocumentIdentifier, position: Position): Async = async { let file = FileInfo(textDocument.uri.LocalPath) let! c = checkOpenFile(file, true, false) let line = lineContent(file, position.line) let maybeId = QuickParse.GetCompleteIdentifierIsland false line (position.character) - match c, maybeId with - | Error(errors), _ -> + match c, maybeId with + | Error(errors), _ -> dprintfn "Check failed, ignored %d errors" (List.length errors) return None - | _, None -> - dprintfn "No identifier at %d in line '%s'" position.character line + | _, None -> + dprintfn "No identifier at %d in line '%s'" position.character line return None - | Ok(_, checkResult), Some(id, endOfIdentifier, _) -> + | Ok(_, checkResult), Some(id, endOfIdentifier, _) -> dprintfn "Looking at symbol %s" id let names = List.ofArray(id.Split('.')) - let! maybeSymbol = checkResult.GetSymbolUseAtLocation(position.line+1, endOfIdentifier, line, names) + let maybeSymbol = checkResult.GetSymbolUseAtLocation(position.line+1, endOfIdentifier, line, names) if maybeSymbol.IsNone then dprintfn "%s in line '%s' is not a symbol use" id line return maybeSymbol @@ -363,117 +361,117 @@ type Server(client: ILanguageClient) = /// Find the exact location of a symbol within a fully-qualified name. /// For example, if we have `let b = Foo.bar`, and we want to find the symbol `bar` in the range `let b = [Foo.bar]`. - let refineRenameRange(s: FSharpSymbol, file: FileInfo, range: Range.range): Range = + let refineRenameRange(s: FSharpSymbol, file: FileInfo, range: Text.range): Range = let line = range.End.Line - 1 let startColumn = if range.Start.Line - 1 < line then 0 else range.Start.Column let endColumn = range.End.Column let lineText = lineContent(file, line ) let find = lineText.LastIndexOf(s.DisplayName, endColumn, endColumn - startColumn) if find = -1 then - dprintfn "Couldn't find '%s' in line '%s'" s.DisplayName lineText + dprintfn "Couldn't find '%s' in line '%s'" s.DisplayName lineText asRange range - else + else { start={line=line; character=find} ``end``={line=line; character=find + s.DisplayName.Length} } /// Rename one usage of a symbol - let renameTo(newName: string, file: FileInfo, usages: FSharpSymbolUse seq): TextDocumentEdit = + let renameTo(newName: string, file: FileInfo, usages: FSharpSymbolUse seq): TextDocumentEdit = let uri = Uri("file://" + file.FullName) let version = docs.GetVersion(file) |> Option.defaultValue 0 let edits = [ - for u in usages do + for u in usages do let range = refineRenameRange(u.Symbol, FileInfo(u.FileName), u.RangeAlternate) yield {range=range; newText=newName} ] {textDocument={uri=uri; version=version}; edits=edits} let symbolPattern = Regex(@"\w+") /// Quickly check if a file *might* contain a symbol matching query - let maybeMatchesQuery(query: string, file: FileInfo): string option = - match getOrRead(file) with - | None -> None - | Some(text) -> + let maybeMatchesQuery(query: string, file: FileInfo): string option = + match getOrRead(file) with + | None -> None + | Some(text) -> let matches = symbolPattern.Matches(text) let test(m: Match) = matchesQuery(query, m.Value) - if Seq.exists test matches then - Some(text) - else + if Seq.exists test matches then + Some(text) + else None let exactlyMatches(findSymbol: string, file: FileInfo): string option = - match getOrRead(file) with - | None -> None - | Some(text) -> + match getOrRead(file) with + | None -> None + | Some(text) -> let matches = symbolPattern.Matches(text) let test(m: Match) = m.Value = findSymbol - if Seq.exists test matches then - Some(text) - else + if Seq.exists test matches then + Some(text) + else None /// Find all uses of a symbol, across all open projects - let findAllSymbolUses(symbol: FSharpSymbol): Async> = + let findAllSymbolUses(symbol: FSharpSymbol): Async> = async { // If the symbol is private or internal, we only need to scan 1 file or project // TODO this only detects symbols *declared* private, many symbols are implicitly private - let isPrivate, isInternal = - match FSharpSymbol.GetAccessibility(symbol) with + let isPrivate, isInternal = + match FSharpSymbol.GetAccessibility(symbol) with | Some(a) when a.IsPrivate -> true, true | Some(a) when a.IsInternal -> false, true | _ -> false, false // Figure out what project and file the symbol is declared in // This might be nothing if the symbol is declared outside the workspace - let symbolDeclarationProject, symbolDeclarationFile = - match symbol.DeclarationLocation with + let symbolDeclarationProject, symbolDeclarationFile = + match symbol.DeclarationLocation with | None -> None, None - | Some(range) -> + | Some(range) -> let f = FileInfo(range.FileName) - match projects.FindProjectOptions(f) with + match projects.FindProjectOptions(f) with | Error(_) -> None, Some(f) | Ok(projectOptions) -> Some(projectOptions), Some(f) - if isPrivate then + if isPrivate then dprintfn "Symbol %s is private so we will only check declaration file %A" symbol.FullName symbolDeclarationFile - elif isInternal then + elif isInternal then dprintfn "Symbol %s is internal so we will onlcy check declaration project %A" symbol.FullName symbolDeclarationProject // Is fileName the same file symbol was declared in? let isSymbolFile(fileName: string) = match symbolDeclarationFile with None -> false | Some(f) -> f.FullName = fileName // Is candidate the same project that symbol was declared in? - let isSymbolProject(candidate: FSharpProjectOptions) = + let isSymbolProject(candidate: FSharpProjectOptions) = match symbolDeclarationProject with None -> false | Some(p) -> candidate.ProjectFileName = p.ProjectFileName // Does fileName come after symbol in dependency order, meaning it can see symbol? - let isVisibleFromFile(fromFile: FileInfo) = - match symbolDeclarationFile with + let isVisibleFromFile(fromFile: FileInfo) = + match symbolDeclarationFile with | Some(symbolFile) -> projects.IsVisible(symbolFile, fromFile) | _ -> true // Is symbol visible from file? - let isVisibleFrom(project: FSharpProjectOptions, file: string) = - if isPrivate then + let isVisibleFrom(project: FSharpProjectOptions, file: string) = + if isPrivate then isSymbolFile(file) - elif isInternal then + elif isInternal then isSymbolProject(project) && isVisibleFromFile(FileInfo(file)) - else + else isVisibleFromFile(FileInfo(file)) // Find all source files that can see symbol let visible = [ - for projectOptions in projects.OpenProjects do - for fileName in projectOptions.SourceFiles do - if isVisibleFrom(projectOptions, fileName) then + for projectOptions in projects.OpenProjects do + for fileName in projectOptions.SourceFiles do + if isVisibleFrom(projectOptions, fileName) then yield projectOptions, FileInfo(fileName) ] let visibleNames = String.concat ", " [for _, f in visible do yield f.Name] dprintfn "Symbol %s is visible from %s" symbol.FullName visibleNames // Check source files for possible symbol references using string matching - let searchFor = + let searchFor = // Attributes are referenced without the `Attribute` suffix // searchFor is just used to cut down the number of files we need to check, // so it's OK to be a little fast-and-sloppy - if symbol.DisplayName.EndsWith("Attribute") then + if symbol.DisplayName.EndsWith("Attribute") then symbol.DisplayName.Substring(0, symbol.DisplayName.Length - "Attribute".Length) - else + else symbol.DisplayName let candidates = [ - for projectOptions, sourceFile in visible do - match exactlyMatches(searchFor, sourceFile) with + for projectOptions, sourceFile in visible do + match exactlyMatches(searchFor, sourceFile) with | None -> () | Some(sourceText) -> yield projectOptions, sourceFile, sourceText ] @@ -482,52 +480,52 @@ type Server(client: ILanguageClient) = // Check each candidate file use progress = new ProgressBar(candidates.Length, sprintf "Search %d files" candidates.Length, client) let all = System.Collections.Generic.List() - for projectOptions, sourceFile, sourceText in candidates do + for projectOptions, sourceFile, sourceText in candidates do try // Send a notification to the client updating the progress indicator progress.Increment(sourceFile) - + // Check file let sourceVersion = docs.GetVersion(sourceFile) |> Option.defaultValue 0 let timeCheck = Stopwatch.StartNew() let! _, maybeCheck = checker.ParseAndCheckFileInProject(sourceFile.FullName, sourceVersion, SourceText.ofString(sourceText), projectOptions) dprintfn "Checked %s in %dms" sourceFile.Name timeCheck.ElapsedMilliseconds - match maybeCheck with + match maybeCheck with | FSharpCheckFileAnswer.Aborted -> dprintfn "Aborted checking %s" sourceFile.Name - | FSharpCheckFileAnswer.Succeeded(check) -> - let! uses = check.GetUsesOfSymbolInFile(symbol) - for u in uses do + | FSharpCheckFileAnswer.Succeeded(check) -> + let uses = check.GetUsesOfSymbolInFile(symbol) + for u in uses do all.Add(u) - with e -> + with e -> dprintfn "Error checking %s: %s" sourceFile.Name e.Message return List.ofSeq(all) } /// Tell the user if we run out of memory /// TODO add a setting to increase max memory - let maxMemoryWarning() = + let maxMemoryWarning() = let message = sprintf "Reached max memory %d MB" checker.MaxMemory client.ShowMessage({``type``=MessageType.Warning; message=message}) let _ = checker.MaxMemoryReached.Add(maxMemoryWarning) /// Remember the last completion list for ResolveCompletionItem - let mutable lastCompletion: FSharpDeclarationListInfo option = None + let mutable lastCompletion: FSharpDeclarationListInfo option = None /// Defer initialization operations until Initialized() is called, /// so that the client-side code int client/extension.ts starts running immediately let mutable deferredInitialize = async { () } - interface ILanguageServer with + interface ILanguageServer with member this.Initialize(p: InitializeParams) = async { - match p.rootUri with - | Some root -> + match p.rootUri with + | Some root -> dprintfn "Add workspace root %s" root.LocalPath - deferredInitialize <- projects.AddWorkspaceRoot(DirectoryInfo(root.LocalPath)) + deferredInitialize <- projects.AddWorkspaceRoot(DirectoryInfo(root.LocalPath)) | _ -> dprintfn "No root URI in initialization message %A" p - return { - capabilities = - { defaultServerCapabilities with + return { + capabilities = + { defaultServerCapabilities with hoverProvider = true completionProvider = Some({resolveProvider=true; triggerCharacters=['.']}) signatureHelpProvider = Some({triggerCharacters=['('; ',']}) @@ -537,24 +535,24 @@ type Server(client: ILanguageClient) = definitionProvider = true referencesProvider = true renameProvider = true - textDocumentSync = - { defaultTextDocumentSyncOptions with - openClose = true + textDocumentSync = + { defaultTextDocumentSyncOptions with + openClose = true save = Some({ includeText = false }) - change = TextDocumentSyncKind.Incremental - } + change = TextDocumentSyncKind.Incremental + } } } } - member this.Initialized(): Async = + member this.Initialized(): Async = deferredInitialize - member this.Shutdown(): Async = + member this.Shutdown(): Async = async { () } member this.DidChangeConfiguration(p: DidChangeConfigurationParams): Async = async { dprintfn "New configuration %s" (p.ToString()) } - member this.DidOpenTextDocument(p: DidOpenTextDocumentParams): Async = + member this.DidOpenTextDocument(p: DidOpenTextDocumentParams): Async = async { let file = FileInfo(p.textDocument.uri.LocalPath) // Store text in docs @@ -565,7 +563,7 @@ type Server(client: ILanguageClient) = use increment = checker.BeforeBackgroundFileCheck.Subscribe(fun (fileName, _) -> progress.Increment(FileInfo(fileName))) do! doCheck(file) } - member this.DidChangeTextDocument(p: DidChangeTextDocumentParams): Async = + member this.DidChangeTextDocument(p: DidChangeTextDocumentParams): Async = async { let file = FileInfo(p.textDocument.uri.LocalPath) docs.Change(p) @@ -573,53 +571,53 @@ type Server(client: ILanguageClient) = } member this.WillSaveTextDocument(p: WillSaveTextDocumentParams): Async = TODO() member this.WillSaveWaitUntilTextDocument(p: WillSaveTextDocumentParams): Async = TODO() - member this.DidSaveTextDocument(p: DidSaveTextDocumentParams): Async = + member this.DidSaveTextDocument(p: DidSaveTextDocumentParams): Async = async { let targetFile = FileInfo(p.textDocument.uri.LocalPath) - let todo = [ for fromFile in docs.OpenFiles() do - if projects.IsVisible(targetFile, fromFile) then + let todo = [ for fromFile in docs.OpenFiles() do + if projects.IsVisible(targetFile, fromFile) then yield fromFile ] use progress = new ProgressBar(todo.Length, sprintf "Check %d files" todo.Length, client, todo.Length <= 1) - for file in todo do + for file in todo do progress.Increment(file) let! check = checkOpenFile(file, false, false) let! errors = getErrors(file, check) publishErrors(file, errors) } - member this.DidCloseTextDocument(p: DidCloseTextDocumentParams): Async = + member this.DidCloseTextDocument(p: DidCloseTextDocumentParams): Async = async { let file = FileInfo(p.textDocument.uri.LocalPath) docs.Close(p) // Only show errors for open files publishErrors(file, []) } - member this.DidChangeWatchedFiles(p: DidChangeWatchedFilesParams): Async = + member this.DidChangeWatchedFiles(p: DidChangeWatchedFilesParams): Async = async { - for change in p.changes do + for change in p.changes do let file = FileInfo(change.uri.LocalPath) dprintfn "Watched file %s %O" file.FullName change.``type`` - if file.Name.EndsWith(".fsproj") || file.Name.EndsWith(".fsx") then - match change.``type`` with + if file.Name.EndsWith(".fsproj") || file.Name.EndsWith(".fsx") then + match change.``type`` with | FileChangeType.Created -> projects.NewProjectFile(file) - | FileChangeType.Changed -> + | FileChangeType.Changed -> projects.UpdateProjectFile(file) | FileChangeType.Deleted -> projects.DeleteProjectFile(file) elif file.Name.EndsWith(".sln") then - match change.``type`` with + match change.``type`` with | FileChangeType.Created -> projects.UpdateSlnFile(file) - | FileChangeType.Changed -> + | FileChangeType.Changed -> projects.UpdateSlnFile(file) | FileChangeType.Deleted -> projects.DeleteSlnFile(file) - elif file.Name = "project.assets.json" then + elif file.Name = "project.assets.json" then projects.UpdateAssetsJson(file) // Re-check all open files - // In theory we could optimize this by only re-checking descendents of changed projects, + // In theory we could optimize this by only re-checking descendents of changed projects, // but in practice that will make little difference - for f in docs.OpenFiles() do + for f in docs.OpenFiles() do backgroundCheck.CheckLater(f) } member this.Completion(p: TextDocumentPositionParams): Async = @@ -635,71 +633,71 @@ type Server(client: ILanguageClient) = // TODO when this is the only edited line, and the line looks like x.y.z.?, then stale completions are quite accurate let! c = checkOpenFile(file, true, not(noPartialName)) dprintfn "Finished typecheck, looking for completions..." - match c with - | Error errors -> + match c with + | Error errors -> dprintfn "Check failed, ignored %d errors" (List.length(errors)) return None - | Ok(parseResult, checkResult) -> - let! declarations = checkResult.GetDeclarationListInfo(Some parseResult, p.position.line+1, line, partialName) - lastCompletion <- Some declarations + | Ok(parseResult, checkResult) -> + let declarations = checkResult.GetDeclarationListInfo(Some parseResult, p.position.line+1, line, partialName) + lastCompletion <- Some declarations dprintfn "Found %d completions" declarations.Items.Length return Some(asCompletionList(declarations)) } - member this.Hover(p: TextDocumentPositionParams): Async = + member this.Hover(p: TextDocumentPositionParams): Async = async { let file = FileInfo(p.textDocument.uri.LocalPath) let! c = checkOpenFile(file, true, false) let line = lineContent(file, p.position.line) let maybeId = QuickParse.GetCompleteIdentifierIsland false line (p.position.character) - match c, maybeId with - | Error(errors), _ -> + match c, maybeId with + | Error(errors), _ -> dprintfn "Check failed, ignored %d errors" (List.length(errors)) return None - | _, None -> + | _, None -> dprintfn "No identifier at %s(%d, %d)" file.FullName p.position.line p.position.character return None - | Ok(parseResult, checkResult), Some(id, _, _) -> + | Ok(parseResult, checkResult), Some(id, _, _) -> dprintfn "Hover over %s" id let ids = List.ofArray(id.Split('.')) - let! tips = checkResult.GetToolTipText(p.position.line+1, p.position.character+1, line, ids, FSharpTokenTag.Identifier) + let tips = checkResult.GetToolTipText(p.position.line+1, p.position.character+1, line, ids, FSharpTokenTag.Identifier) return Some(asHover(tips)) } // Add documentation to a completion item // Generating documentation is an expensive step, so we want to defer it until the user is actually looking at it - member this.ResolveCompletionItem(p: CompletionItem): Async = + member this.ResolveCompletionItem(p: CompletionItem): Async = async { let mutable result = p - if lastCompletion.IsSome then - for candidate in lastCompletion.Value.Items do - if candidate.FullName = p.data?FullName.AsString() then + if lastCompletion.IsSome then + for candidate in lastCompletion.Value.Items do + if candidate.FullName = p.data?FullName.AsString() then dprintfn "Resolve description for %s" candidate.FullName let! resolved = TipFormatter.resolveDocs(p, candidate) result <- resolved return result } - member this.SignatureHelp(p: TextDocumentPositionParams): Async = + member this.SignatureHelp(p: TextDocumentPositionParams): Async = async { let file = FileInfo(p.textDocument.uri.LocalPath) let! c = checkOpenFile(file, true, true) - match c with - | Error errors -> + match c with + | Error errors -> dprintfn "Check failed, ignored %d errors" (List.length(errors)) return None - | Ok(parseResult, checkResult) -> + | Ok(parseResult, checkResult) -> let line = lineContent(file, p.position.line) - match findMethodCallBeforeCursor(line, p.position.character) with - | None -> - dprintfn "No method call in line %s" line + match findMethodCallBeforeCursor(line, p.position.character) with + | None -> + dprintfn "No method call in line %s" line return None - | Some endOfMethodName -> - match QuickParse.GetCompleteIdentifierIsland false line (endOfMethodName - 1) with - | None -> + | Some endOfMethodName -> + match QuickParse.GetCompleteIdentifierIsland false line (endOfMethodName - 1) with + | None -> dprintfn "No identifier before column %d in %s" (endOfMethodName - 1) line - return None + return None | Some(id, _, _) -> dprintfn "Looking for overloads of %s" id let names = List.ofArray(id.Split('.')) - let! overloads = checkResult.GetMethods(p.position.line+1, endOfMethodName, line, Some names) + let overloads = checkResult.GetMethods(p.position.line+1, endOfMethodName, line, Some names) let signature(i: FSharpMethodGroupItem) = asSignatureInformation(overloads.MethodName, i) let sigs = Array.map signature overloads.Methods |> List.ofArray let activeParameter = countCommas(line, endOfMethodName, p.position.character) @@ -707,19 +705,19 @@ type Server(client: ILanguageClient) = dprintfn "Found %d overloads" overloads.Methods.Length return Some({signatures=sigs; activeSignature=activeDeclaration; activeParameter=Some activeParameter}) } - member this.GotoDefinition(p: TextDocumentPositionParams): Async = + member this.GotoDefinition(p: TextDocumentPositionParams): Async = async { let! maybeSymbol = symbolAt(p.textDocument, p.position) - match maybeSymbol with + match maybeSymbol with | None -> return [] | Some s -> return declarationLocation s.Symbol |> Option.toList } - member this.FindReferences(p: ReferenceParams): Async = + member this.FindReferences(p: ReferenceParams): Async = async { let! maybeSymbol = symbolAt(p.textDocument, p.position) - match maybeSymbol with - | None -> return [] - | Some s -> + match maybeSymbol with + | None -> return [] + | Some s -> let! uses = findAllSymbolUses(s.Symbol) return List.map useLocation uses } @@ -728,84 +726,84 @@ type Server(client: ILanguageClient) = async { let file = FileInfo(p.textDocument.uri.LocalPath) let! maybeParse = parseFile(file) - match maybeParse with - | Error e -> - dprintfn "%s" e + match maybeParse with + | Error e -> + dprintfn "%s" e return [] | Ok parse -> let flat = findDeclarations(parse) return List.map asSymbolInformation flat } - member this.WorkspaceSymbols(p: WorkspaceSymbolParams): Async = + member this.WorkspaceSymbols(p: WorkspaceSymbolParams): Async = async { dprintfn "Looking for symbols matching `%s`" p.query // Read open projects until we find at least 50 symbols that match query let all = System.Collections.Generic.List() // TODO instead of checking open projects, check all .fs files, using default parsing options - for projectOptions in projects.OpenProjects do + for projectOptions in projects.OpenProjects do dprintfn "...check project %s" projectOptions.ProjectFileName - for sourceFileName in projectOptions.SourceFiles do + for sourceFileName in projectOptions.SourceFiles do let sourceFile = FileInfo(sourceFileName) - if all.Count < 50 then + if all.Count < 50 then dprintfn "...scan %s" sourceFile.Name - match maybeMatchesQuery(p.query, sourceFile) with - | None -> () + match maybeMatchesQuery(p.query, sourceFile) with + | None -> () | Some(sourceText) -> try dprintfn "...parse %s" sourceFile.Name let parsingOptions, _ = checker.GetParsingOptionsFromProjectOptions(projectOptions) let! parse = checker.ParseFile(sourceFile.FullName, SourceText.ofString(sourceText), parsingOptions) - for declaration, container in findDeclarations(parse) do - if matchesQuery(p.query, declaration.Name) then + for declaration, container in findDeclarations(parse) do + if matchesQuery(p.query, declaration.Name) then all.Add(asSymbolInformation(declaration, container)) - with e -> + with e -> dprintfn "Error parsing %s: %s" sourceFile.Name e.Message return List.ofSeq(all) } member this.CodeActions(p: CodeActionParams): Async = TODO() - member this.CodeLens(p: CodeLensParams): Async> = + member this.CodeLens(p: CodeLensParams): Async> = async { let file = FileInfo(p.textDocument.uri.LocalPath) - match projects.FindProjectOptions(file), getOrRead(file) with - | Ok(projectOptions), Some(sourceText) -> + match projects.FindProjectOptions(file), getOrRead(file) with + | Ok(projectOptions), Some(sourceText) -> let parsingOptions, _ = checker.GetParsingOptionsFromProjectOptions(projectOptions) let! parse = checker.ParseFile(file.FullName, SourceText.ofString(sourceText), parsingOptions) - if file.Name.EndsWith(".fs") then + if file.Name.EndsWith(".fs") then let fns = testFunctions(parse) let fsproj = FileInfo(projectOptions.ProjectFileName) - return [ for id, bindings in fns do + return [ for id, bindings in fns do yield asRunTest(fsproj, id, bindings) yield asDebugTest(fsproj, id, bindings) ] - else if file.Name.EndsWith(".fsi") then - return - [ for name, range in findSignatureDeclarations(parse) do + else if file.Name.EndsWith(".fsi") then + return + [ for name, range in findSignatureDeclarations(parse) do yield asGoToImplementation(name, file, range) ] - else + else dprintfn "Don't know how to compute code lenses on extension %s" file.Extension return [] - | Error(e), _ -> + | Error(e), _ -> dprintfn "Failed to create code lens because project options failed to load: %A" e return [] - | _, None -> + | _, None -> dprintfn "Failed to create code lens because file %s does not exist" file.FullName return [] } - member this.ResolveCodeLens(p: CodeLens): Async = + member this.ResolveCodeLens(p: CodeLens): Async = async { - if p.data <> JsonValue.Null then + if p.data <> JsonValue.Null then dprintfn "Resolving %A" p.data let fsi, name = goToImplementationData(p) - if not(fsi.Extension = ".fsi") then + if not(fsi.Extension = ".fsi") then raise(Exception(sprintf "Signature file %s should end with .fsi" fsi.Name)) let file = FileInfo(fsi.FullName.Substring(0, fsi.FullName.Length - 1)) - match projects.FindProjectOptions(file), getOrRead(file) with - | Ok(projectOptions), Some(sourceText) -> + match projects.FindProjectOptions(file), getOrRead(file) with + | Ok(projectOptions), Some(sourceText) -> let parsingOptions, _ = checker.GetParsingOptionsFromProjectOptions(projectOptions) let! parse = checker.ParseFile(file.FullName, SourceText.ofString(sourceText), parsingOptions) - match findSignatureImplementation(parse, name) with - | [range] -> + match findSignatureImplementation(parse, name) with + | [range] -> return resolveGoToImplementation(p, file, range) - | [] -> + | [] -> dprintfn "Signature %A has no implementation in %s" name file.Name return resolveMissingGoToImplementation(p, fsi) | many -> @@ -814,10 +812,10 @@ type Server(client: ILanguageClient) = // This is wrong but still useful let range = many.Head return resolveGoToImplementation(p, file, range) - | Error(e), _ -> + | Error(e), _ -> dprintfn "Failed to resolve code lens because project options failed to load: %A" e return p - | _, None -> + | _, None -> dprintfn "Failed to resolve code lens because file %s does not exist" file.FullName return p else return p @@ -830,9 +828,9 @@ type Server(client: ILanguageClient) = member this.Rename(p: RenameParams): Async = async { let! maybeSymbol = symbolAt(p.textDocument, p.position) - match maybeSymbol with + match maybeSymbol with | None -> return {documentChanges=[]} - | Some s -> + | Some s -> let! uses = findAllSymbolUses(s.Symbol) let byFile = List.groupBy (fun (usage:FSharpSymbolUse) -> usage.FileName) uses let fileNames = List.map fst byFile @@ -841,9 +839,9 @@ type Server(client: ILanguageClient) = return {documentChanges=List.ofSeq(renames)} } member this.ExecuteCommand(p: ExecuteCommandParams): Async = TODO() - member this.DidChangeWorkspaceFolders(p: DidChangeWorkspaceFoldersParams): Async = + member this.DidChangeWorkspaceFolders(p: DidChangeWorkspaceFoldersParams): Async = async { - for root in p.event.added do + for root in p.event.added do let file = FileInfo(root.uri.LocalPath) do! projects.AddWorkspaceRoot(file.Directory) // TODO removed @@ -855,9 +853,9 @@ let main(argv: array): int = let write = new BinaryWriter(Console.OpenStandardOutput()) let serverFactory(client) = Server(client) :> ILanguageServer dprintfn "Listening on stdin" - try + try LanguageServer.connect(serverFactory, read, write) 0 // return an integer exit code - with e -> + with e -> dprintfn "Exception in language server %O" e 1 diff --git a/src/FSharpLanguageServer/ProjectManager.fs b/src/FSharpLanguageServer/ProjectManager.fs index 11106ed..3f0d3ae 100644 --- a/src/FSharpLanguageServer/ProjectManager.fs +++ b/src/FSharpLanguageServer/ProjectManager.fs @@ -1,4 +1,4 @@ -namespace FSharpLanguageServer +namespace FSharpLanguageServer open LSP.Log open System @@ -18,36 +18,36 @@ type private ResolvedProject = { sources: FileInfo list options: FSharpProjectOptions target: FileInfo - errors: Diagnostic list + errors: Diagnostic list } type private LazyProject = { - file: FileInfo + file: FileInfo resolved: Lazy } -type private ProjectCache() = +type private ProjectCache() = let knownProjects = new Dictionary() - member this.Invalidate(fsprojOrFsx: FileInfo) = + member this.Invalidate(fsprojOrFsx: FileInfo) = knownProjects.Remove(fsprojOrFsx.FullName) |> ignore - member this.Get(fsprojOrFsx: FileInfo, analyzeLater: FileInfo -> LazyProject): LazyProject = - if not(knownProjects.ContainsKey(fsprojOrFsx.FullName)) then + member this.Get(fsprojOrFsx: FileInfo, analyzeLater: FileInfo -> LazyProject): LazyProject = + if not(knownProjects.ContainsKey(fsprojOrFsx.FullName)) then knownProjects.Add(fsprojOrFsx.FullName, analyzeLater(fsprojOrFsx)) knownProjects.[fsprojOrFsx.FullName] /// Maintains caches of parsed versions of .fsprojOrFsx files -type ProjectManager(checker: FSharpChecker) = +type ProjectManager(checker: FSharpChecker) = /// Remember what .fsproj files are referenced by .sln files /// Keys are full paths to .sln files /// Values are lists of .fsproj files referenced by the .sln file let knownSolutions = new Dictionary>() - /// Remember what .fsproj and .fsx files are present + /// Remember what .fsproj and .fsx files are present let knownProjects = new HashSet() //// Cache expensive analyze operations let cache = ProjectCache() - let printOptions(options: FSharpProjectOptions) = + let printOptions(options: FSharpProjectOptions) = // This is long but it's useful dprintfn "%s: " options.ProjectFileName dprintfn " ProjectFileName: %A" options.ProjectFileName @@ -65,21 +65,21 @@ type ProjectManager(checker: FSharpChecker) = /// When was this .fsx, .fsproj or corresponding project.assets.json file modified? // TODO use checksum instead of time - let lastModified(fsprojOrFsx: FileInfo) = + let lastModified(fsprojOrFsx: FileInfo) = let assets = FileInfo(Path.Combine [| fsprojOrFsx.Directory.FullName; "obj"; "project.assets.json" |]) - if assets.Exists then + if assets.Exists then max fsprojOrFsx.LastWriteTime assets.LastWriteTime - else + else fsprojOrFsx.LastWriteTime /// Find any .fsproj files associated with a project.assets.json - let projectFileForAssets(assetsJson: FileInfo) = + let projectFileForAssets(assetsJson: FileInfo) = let dir = assetsJson.Directory.Parent dir.GetFiles("*.fsproj") /// Find base dlls /// Workaround of https://github.com/fsharp/FSharp.Compiler.Service/issues/847 - let dotNetFramework = + let dotNetFramework = let dir = Path.GetDirectoryName(typeof.Assembly.Location) let relative = [ "FSharp.Core.dll" @@ -226,30 +226,30 @@ type ProjectManager(checker: FSharpChecker) = "mscorlib.dll" "netstandard.dll" ] - [ for d in relative do + [ for d in relative do let f = FileInfo(Path.Combine(dir, d)) - if f.Exists then + if f.Exists then yield f - else - dprintfn "Couldn't find %s in %s" d dir + else + dprintfn "Couldn't find %s in %s" d dir ] /// Analyze a .fsx or .fsproj file - let rec analyzeLater(fsprojOrFsx: FileInfo): LazyProject = + let rec analyzeLater(fsprojOrFsx: FileInfo): LazyProject = /// Analyze a script file - let analyzeFsx(fsx: FileInfo) = + let analyzeFsx(fsx: FileInfo) = dprintfn "Creating project options for script %s" fsx.Name let source = SourceText.ofString(File.ReadAllText(fsx.FullName)) let inferred, errors = checker.GetProjectOptionsFromScript(fsx.FullName, source, loadedTimeStamp=fsx.LastWriteTime, assumeDotNetFramework=false) |> Async.RunSynchronously let combinedOtherOptions = [| - for p in dotNetFramework do + for p in dotNetFramework do yield "-r:" + p.FullName - for o in inferred.OtherOptions do + for o in inferred.OtherOptions do // If a dll is included by default, skip it let matchesName(f: FileInfo) = o.EndsWith(f.Name) let alreadyIncluded = List.exists matchesName dotNetFramework if not(alreadyIncluded) then - yield o + yield o |] let options = {inferred with OtherOptions = combinedOtherOptions} printOptions(options) @@ -260,85 +260,85 @@ type ProjectManager(checker: FSharpChecker) = errors=Conversions.asDiagnostics(errors) } /// Analyze a project - let analyzeFsproj(fsproj: FileInfo) = + let analyzeFsproj(fsproj: FileInfo) = dprintfn "Analyzing %s" fsproj.Name let cracked = ProjectCracker.crack(fsproj) // Convert to FSharpProjectOptions let options = { - ExtraProjectInfo = None - IsIncompleteTypeCheckEnvironment = false + ExtraProjectInfo = None + IsIncompleteTypeCheckEnvironment = false LoadTime = lastModified(fsproj) OriginalLoadReferences = [] - OtherOptions = + OtherOptions = [| // Dotnet framework should be specified explicitly yield "--noframework" // Reference output of other projects - for r in cracked.projectReferences do + for r in cracked.projectReferences do let options = cache.Get(r, analyzeLater) yield "-r:" + options.resolved.Value.target.FullName // Reference target .dll for .csproj proejcts - for r in cracked.otherProjectReferences do + for r in cracked.otherProjectReferences do yield "-r:" + r.FullName // Reference packages - for r in cracked.packageReferences do + for r in cracked.packageReferences do yield "-r:" + r.FullName // Direct dll references - for r in cracked.directReferences do + for r in cracked.directReferences do yield "-r:" + r.FullName |] - ProjectFileName = fsproj.FullName + ProjectFileName = fsproj.FullName ProjectId = None // This is apparently relevant to multi-targeting builds https://github.com/Microsoft/visualfsharp/pull/4918 - ReferencedProjects = + ReferencedProjects = [| - for r in cracked.projectReferences do + for r in cracked.projectReferences do let options = cache.Get(r, analyzeLater) yield options.resolved.Value.target.FullName, options.resolved.Value.options |] - SourceFiles = + SourceFiles = [| - for f in cracked.sources do + for f in cracked.sources do yield f.FullName |] - Stamp = None - UnresolvedReferences = None + Stamp = None + UnresolvedReferences = None UseScriptResolutionRules = false } // Log what we inferred printOptions(options) { sources=cracked.sources - options=options + options=options target=cracked.target errors=match cracked.error with None -> [] | Some(e) -> [Conversions.errorAtTop(e)] } // Direct to analyzeFsx or analyzeFsproj, depending on type - if fsprojOrFsx.Name.EndsWith(".fsx") then + if fsprojOrFsx.Name.EndsWith(".fsx") then {file=fsprojOrFsx; resolved=lazy(analyzeFsx(fsprojOrFsx))} - elif fsprojOrFsx.Name.EndsWith(".fsproj") then + elif fsprojOrFsx.Name.EndsWith(".fsproj") then {file=fsprojOrFsx; resolved=lazy(analyzeFsproj(fsprojOrFsx))} - else + else raise(Exception(sprintf "Don't know how to analyze project %s" fsprojOrFsx.Name)) /// Invalidate all descendents of a modified .fsproj or .fsx file - let invalidateDescendents(fsprojOrFsx: FileInfo) = + let invalidateDescendents(fsprojOrFsx: FileInfo) = cache.Invalidate(fsprojOrFsx) - for fileName in knownProjects do + for fileName in knownProjects do let file = FileInfo(fileName) let project = cache.Get(file, analyzeLater) - if project.resolved.IsValueCreated then - for _, ancestor in project.resolved.Value.options.ReferencedProjects do - if ancestor.ProjectFileName = fsprojOrFsx.FullName then + if project.resolved.IsValueCreated then + for _, ancestor in project.resolved.Value.options.ReferencedProjects do + if ancestor.ProjectFileName = fsprojOrFsx.FullName then dprintfn "%s has been invalidated by changes to %s" ancestor.ProjectFileName fsprojOrFsx.Name cache.Invalidate(FileInfo(ancestor.ProjectFileName)) /// All transitive deps of anproject, including itself - let transitiveDeps(fsprojOrFsx: FileInfo) = + let transitiveDeps(fsprojOrFsx: FileInfo) = let touched = new HashSet() let result = new List() - let rec walk(options: FSharpProjectOptions) = - if touched.Add(options.ProjectFileName) then - for _, parent in options.ReferencedProjects do + let rec walk(options: FSharpProjectOptions) = + if touched.Add(options.ProjectFileName) then + for _, parent in options.ReferencedProjects do walk(parent) result.Add(options) let root = cache.Get(fsprojOrFsx, analyzeLater) @@ -348,17 +348,19 @@ type ProjectManager(checker: FSharpChecker) = /// Find all .fsproj files referenced by a .sln file let slnProjectReferences (sln: FileInfo): list = // From https://github.com/OmniSharp/omnisharp-roslyn/blob/master/src/OmniSharp.MSBuild/SolutionParsing/ProjectBlock.cs - let projectHeader = Regex( - "^" // Beginning of line - + "Project\\(\"(?.*)\"\\)" - + "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace - + "\"(?.*)\"" - + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace - + "\"(?.*)\"" - + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace - + "\"(?.*)\"" - + "$", // End-of-line - RegexOptions.Compiled) + let projectHeader = + Regex( + "^" // Beginning of line + + "Project\\(\"(?.*)\"\\)" + + "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace + + "\"(?.*)\"" + + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace + + "\"(?.*)\"" + + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace + + "\"(?.*)\"" + + "$", // End-of-line + RegexOptions.Compiled + ) [ for line in File.ReadLines sln.FullName do let m = projectHeader.Match(line) if m.Success then @@ -367,35 +369,35 @@ type ProjectManager(checker: FSharpChecker) = let path = Path.Combine(sln.Directory.FullName, relativePath) let normalize = Path.GetFullPath(path) yield FileInfo(normalize) ] - member this.AddWorkspaceRoot(root: DirectoryInfo): Async = + member this.AddWorkspaceRoot(root: DirectoryInfo): Async = async { - for f in root.EnumerateFiles("*.*", SearchOption.AllDirectories) do + for f in root.EnumerateFiles("*.*", SearchOption.AllDirectories) do if f.Name.EndsWith(".fsx") || f.Name.EndsWith(".fsproj") then knownProjects.Add(f.FullName) |> ignore else if f.Name.EndsWith(".sln") then knownSolutions.[f.FullName] <- slnProjectReferences(f) } - member this.DeleteProjectFile(fsprojOrFsx: FileInfo) = + member this.DeleteProjectFile(fsprojOrFsx: FileInfo) = knownProjects.Remove(fsprojOrFsx.FullName) |> ignore cache.Invalidate(fsprojOrFsx) |> ignore invalidateDescendents(fsprojOrFsx) - member this.NewProjectFile(fsprojOrFsx: FileInfo) = + member this.NewProjectFile(fsprojOrFsx: FileInfo) = knownProjects.Add(fsprojOrFsx.FullName) |> ignore invalidateDescendents(fsprojOrFsx) - member this.UpdateProjectFile(fsprojOrFsx: FileInfo) = + member this.UpdateProjectFile(fsprojOrFsx: FileInfo) = invalidateDescendents(fsprojOrFsx) - member this.DeleteSlnFile(sln: FileInfo) = + member this.DeleteSlnFile(sln: FileInfo) = knownSolutions.Remove(sln.FullName) |> ignore - member this.UpdateSlnFile(sln: FileInfo) = + member this.UpdateSlnFile(sln: FileInfo) = knownSolutions.[sln.FullName] <- slnProjectReferences(sln) - member this.UpdateAssetsJson(assets: FileInfo) = + member this.UpdateAssetsJson(assets: FileInfo) = for fsproj in projectFileForAssets(assets) do invalidateDescendents(fsproj) - member this.FindProjectOptions(sourceFile: FileInfo): Result = + member this.FindProjectOptions(sourceFile: FileInfo): Result = let isSourceFile(f: FileInfo) = f.FullName = sourceFile.FullName // Does `p` contain a reference to `sourceFile`? let isMatch(p: ResolvedProject) = List.exists isSourceFile p.sources // Check if the text of `p` contains the name of `sourceFile` without cracking it - let isPotentialMatch(p: LazyProject) = + let isPotentialMatch(p: LazyProject) = let containsFileName(line: string) = line.Contains(sourceFile.Name) let lines = File.ReadAllLines(p.file.FullName) Array.exists containsFileName lines @@ -404,50 +406,50 @@ type ProjectManager(checker: FSharpChecker) = let alreadyCracked, notYetCracked = List.partition isCracked knownProjectsList let isReferencedBySln(fsproj: LazyProject) = seq { - for KeyValue(sln, fsprojs) in knownSolutions do - for f in fsprojs do - if fsproj.file.FullName = f.FullName then + for KeyValue(sln, fsprojs) in knownSolutions do + for f in fsprojs do + if fsproj.file.FullName = f.FullName then dprintfn "%s is referenced by %s" f.Name sln yield sln - } |> Seq.isEmpty |> not + } |> Seq.isEmpty |> not let referencedProjects, orphanProjects = List.partition isReferencedBySln notYetCracked let crackLazily = seq { - // If file is an .fsx, return itself - if sourceFile.Name.EndsWith(".fsx") then + // If file is an .fsx, return itself + if sourceFile.Name.EndsWith(".fsx") then yield cache.Get(sourceFile, analyzeLater) // First, look at all projects that have *already* been cracked - for options in alreadyCracked do - if isMatch(options.resolved.Value) then + for options in alreadyCracked do + if isMatch(options.resolved.Value) then yield options // If that doesn't work, check other .fsproj files dprintfn "No cracked project references %s, looking at uncracked projects..." sourceFile.Name // Prioritize .fsproj files that are referenced by .sln files - for options in referencedProjects@orphanProjects do + for options in referencedProjects@orphanProjects do // Only parse projects that contain the simple name of `sourceFile` - if isPotentialMatch(options) then + if isPotentialMatch(options) then dprintfn "The text of %s contains the string '%s', cracking" options.file.Name sourceFile.Name if isMatch(options.resolved.Value) then yield options } - match Seq.tryHead crackLazily with + match Seq.tryHead crackLazily with | None -> Error([Conversions.errorAtTop(sprintf "No .fsproj or .fsx file references %s" sourceFile.FullName)]) - | Some(options) -> + | Some(options) -> let cracked = options.resolved.Value - if cracked.errors.IsEmpty then + if cracked.errors.IsEmpty then Ok(cracked.options) - else + else Error(cracked.errors) /// All open projects, in dependency order. /// Ancestor projects come before projects that depend on them. - member this.OpenProjects: FSharpProjectOptions list = + member this.OpenProjects: FSharpProjectOptions list = let touched = new HashSet() let result = new List() - let rec walk(options: FSharpProjectOptions) = - if touched.Add(options.ProjectFileName) then - for _, parent in options.ReferencedProjects do + let rec walk(options: FSharpProjectOptions) = + if touched.Add(options.ProjectFileName) then + for _, parent in options.ReferencedProjects do walk(parent) result.Add(options) - for f in knownProjects do + for f in knownProjects do let project = cache.Get(FileInfo(f), analyzeLater) if project.resolved.IsValueCreated then walk(project.resolved.Value.options) @@ -456,12 +458,12 @@ type ProjectManager(checker: FSharpChecker) = member this.TransitiveDeps(projectFile: FileInfo): FSharpProjectOptions list = transitiveDeps(projectFile) /// Is `targetSourceFile` visible from `fromSourceFile`? - member this.IsVisible(targetSourceFile: FileInfo, fromSourceFile: FileInfo) = - match this.FindProjectOptions(fromSourceFile) with - | Error(_) -> false + member this.IsVisible(targetSourceFile: FileInfo, fromSourceFile: FileInfo) = + match this.FindProjectOptions(fromSourceFile) with + | Error(_) -> false | Ok(fromProjectOptions) -> // If fromSourceFile is in the same project as targetSourceFile, check if iFrom comes after iTarget in the source file order - if Array.contains targetSourceFile.FullName fromProjectOptions.SourceFiles then + if Array.contains targetSourceFile.FullName fromProjectOptions.SourceFiles then let iTarget = Array.IndexOf(fromProjectOptions.SourceFiles, targetSourceFile.FullName) let iFrom = Array.IndexOf(fromProjectOptions.SourceFiles, fromSourceFile.FullName) iFrom >= iTarget diff --git a/src/FSharpLanguageServer/SyntaxTreeOps.fs b/src/FSharpLanguageServer/SyntaxTreeOps.fs new file mode 100644 index 0000000..4f0e7b3 --- /dev/null +++ b/src/FSharpLanguageServer/SyntaxTreeOps.fs @@ -0,0 +1,24 @@ +module FSharpLanguageServer.SyntaxTreeOps + +// Forked from https://github.com/dotnet/fsharp/blob/60a2fa663a3c4aed3f03c8bfc6f5e05b04284f23/src/fsharp/range.fs + +open FSharp.Compiler.SyntaxTree +open FSharp.Compiler.Text.Range + +let ident (s, r) = Ident(s, r) + +let textOfId (id: Ident) = id.idText + +let pathOfLid lid = List.map textOfId lid + +let arrPathOfLid lid = Array.ofList (pathOfLid lid) + +let textOfPath path = String.concat "." path + +let textOfLid lid = textOfPath (pathOfLid lid) + +let rangeOfLid (lid: Ident list) = + match lid with + | [] -> failwith "rangeOfLid" + | [id] -> id.idRange + | h :: t -> unionRanges h.idRange (List.last t).idRange diff --git a/src/FSharpLanguageServer/TipFormatter.fs b/src/FSharpLanguageServer/TipFormatter.fs index 2e3d2cf..b83c37c 100644 --- a/src/FSharpLanguageServer/TipFormatter.fs +++ b/src/FSharpLanguageServer/TipFormatter.fs @@ -19,7 +19,7 @@ type private CachedMember = { parameters: (string * string) list returns: string option // For example, ("T:System.ArgumentNullException", "The property is being set to null.") - exceptions: (string * string) list + exceptions: (string * string) list } type private CachedFile = { @@ -29,37 +29,37 @@ type private CachedFile = { let private cache = Dictionary() -let private convertTag(node: HtmlNode, tag: string, mapper: HtmlNode -> HtmlNode) = - let nodes = List.ofSeq(node.Descendants(tag)) - for n in nodes do +let private convertTag(node: HtmlNode, tag: string, mapper: HtmlNode -> HtmlNode) = + let nodes = List.ofSeq(node.Descendants(tag)) + for n in nodes do let parent = n.ParentNode let r = mapper(n) - parent.ReplaceChild(r, n) |> ignore + parent.ReplaceChild(r, n) |> ignore -let private lastWord(text: string): string = +let private lastWord(text: string): string = let wordPattern = Regex(@"[a-zA-Z]\w*") let words = [for m in wordPattern.Matches(text) do yield m.Value] let maybeLast = List.tryLast(words) Option.defaultValue "" maybeLast -let private convertSee(n: HtmlNode): HtmlNode = +let private convertSee(n: HtmlNode): HtmlNode = let ref = n.GetAttributeValue("cref", n.InnerText) let name = lastWord(ref) HtmlNode.CreateNode("`" + name + "`") -let private convertNameToCode(n: HtmlNode): HtmlNode = +let private convertNameToCode(n: HtmlNode): HtmlNode = let name = n.GetAttributeValue("name", n.InnerText) HtmlNode.CreateNode("`" + name + "`") -let private convertInnerTextToCode(n: HtmlNode): HtmlNode = +let private convertInnerTextToCode(n: HtmlNode): HtmlNode = let text = n.InnerText HtmlNode.CreateNode("`" + text + "`") -let private convertPara(n: HtmlNode): HtmlNode = +let private convertPara(n: HtmlNode): HtmlNode = HtmlNode.CreateNode("\n\n") /// Convert special tags listed in https://docs.microsoft.com/en-us/dotnet/csharp/codedoc to markdown -let private convertSpecialTagsToMarkdown(node: HtmlNode) = +let private convertSpecialTagsToMarkdown(node: HtmlNode) = convertTag(node, "see", convertSee) convertTag(node, "paramref", convertNameToCode) convertTag(node, "typeparamref", convertNameToCode) @@ -72,10 +72,10 @@ let private cref(node: HtmlNode) = let parts = attr.Split(':') Array.last(parts) -let private ensure(xmlFile: FileInfo) = - if xmlFile.Exists then - let needsUpdate = - match cache.TryGetValue(xmlFile.FullName) with +let private ensure(xmlFile: FileInfo) = + if xmlFile.Exists then + let needsUpdate = + match cache.TryGetValue(xmlFile.FullName) with | false, _ -> true | _, existing -> existing.loadTime < xmlFile.LastWriteTime if needsUpdate then @@ -86,7 +86,7 @@ let private ensure(xmlFile: FileInfo) = let html = new HtmlDocument() html.Load(xmlFile.FullName) // Find all members - for m in html.DocumentNode.Descendants("member") do + for m in html.DocumentNode.Descendants("member") do convertSpecialTagsToMarkdown(m) let name = m.GetAttributeValue("name", "") let summary = [for e in m.Descendants("summary") do yield e.InnerHtml] @@ -95,105 +95,116 @@ let private ensure(xmlFile: FileInfo) = let exceptions = [for e in m.Descendants("exception") do yield cref(e), e.InnerHtml] parsed.TryAdd(name, { summary = List.tryHead(summary) - parameters = parameters - returns = List.tryHead(returns) + parameters = parameters + returns = List.tryHead(returns) exceptions = exceptions }) |> ignore cache.TryAdd(xmlFile.FullName, { - members = parsed + members = parsed loadTime = xmlFile.LastWriteTime }) |> ignore /// Find the documentation for `memberName` inside of `xmlFile` -let private find(xmlFile: FileInfo, memberName: string): CachedMember option = +let private find(xmlFile: FileInfo, memberName: string): CachedMember option = ensure(xmlFile) - match cache.TryGetValue(xmlFile.FullName) with - | false, _ -> None - | _, cached -> - match cached.members.TryGetValue(memberName) with - | false, _ -> None + match cache.TryGetValue(xmlFile.FullName) with + | false, _ -> None + | _, cached -> + match cached.members.TryGetValue(memberName) with + | false, _ -> None | _, m -> Some(m) /// Render complete documentation, including parameter and return information, for a member that has no overloads let docComment(doc: FSharpXmlDoc): string option = match doc with | FSharpXmlDoc.None -> None - | FSharpXmlDoc.Text(s) -> Some(s) + | FSharpXmlDoc.Text(unprocessedLines, elaboratedXmlLines) -> + unprocessedLines + |> String.concat "\n" + |> Some | FSharpXmlDoc.XmlDocFileSignature(dllPath, memberName) -> let xmlFile = FileInfo(Path.ChangeExtension(dllPath, ".xml")) - match find(xmlFile, memberName) with - | None -> None - | Some(m) -> + match find(xmlFile, memberName) with + | None -> None + | Some(m) -> let lines = [ - if m.summary.IsSome && m.summary.Value.Length > 0 then + if m.summary.IsSome && m.summary.Value.Length > 0 then yield m.summary.Value.Trim() - for name, desc in m.parameters do - yield sprintf "**%s** %s" name desc - if m.returns.IsSome && m.returns.Value.Length > 0 then + for name, desc in m.parameters do + yield sprintf "**%s** %s" name desc + if m.returns.IsSome && m.returns.Value.Length > 0 then yield sprintf "**returns** %s" m.returns.Value - for name, desc in m.exceptions do + for name, desc in m.exceptions do yield sprintf "**exception** `%s` %s" name desc ] let comment = String.concat "\n\n" lines Some(comment) /// Render just the summary documentation -let docSummaryOnly(doc: FSharpXmlDoc): string option = +let docSummaryOnly(doc: FSharpXmlDoc): string option = match doc with | FSharpXmlDoc.None -> None - | FSharpXmlDoc.Text(s) -> Some(s) + | FSharpXmlDoc.Text(unprocessedLines, elaboratedXmlLines) -> + unprocessedLines + |> String.concat "\n" + |> Some | FSharpXmlDoc.XmlDocFileSignature(dllPath, memberName) -> let xmlFile = FileInfo(Path.ChangeExtension(dllPath, ".xml")) - match find(xmlFile, memberName) with - | None -> None + match find(xmlFile, memberName) with + | None -> None | Some(m) -> m.summary |> Option.map (fun(s) -> s.Trim()) /// Render documentation for an overloaded member -let private overloadComment(docs: FSharpXmlDoc list): string option = +let private overloadComment(docs: FSharpXmlDoc list): string option = let summaries = [ - for doc in docs do - match doc with + for doc in docs do + match doc with | FSharpXmlDoc.None -> () - | FSharpXmlDoc.Text(s) -> yield s - | FSharpXmlDoc.XmlDocFileSignature(dllPath, memberName) -> +// | FSharpXmlDoc.Text(s) -> yield s + | FSharpXmlDoc.Text(unprocessedLines, elaboratedXmlLines) -> + yield unprocessedLines + |> String.concat "\n" + | FSharpXmlDoc.XmlDocFileSignature(dllPath, memberName) -> let xmlFile = FileInfo(Path.ChangeExtension(dllPath, ".xml")) - match find(xmlFile, memberName) with - | None -> () - | Some(m) -> - match m.summary with - | None -> () + match find(xmlFile, memberName) with + | None -> () + | Some(m) -> + match m.summary with + | None -> () | Some(message) -> yield message.Trim() ] - let first = List.tryHead summaries - match first with + let first = List.tryHead summaries + match first with | None -> None | Some(summary) -> Some(sprintf "%s\n\n*(%d overloads)*" summary docs.Length) -let private markup(s: string): MarkupContent = +let private markup(s: string): MarkupContent = { kind=MarkupKind.Markdown value=s } /// Add documentation information to the inline help of autocomplete -let resolveDocs(item: CompletionItem, candidate: FSharpDeclarationListItem): Async = +let resolveDocs(item: CompletionItem, candidate: FSharpDeclarationListItem): Async = async { - let! FSharpToolTipText(xs) = candidate.DescriptionTextAsync +// let! FSharpToolTipText(xs) = candidate.DescriptionTextAsync + // TODO: fix this code + let xs = [] // FSharpToolTipText is weirdly nested, unwrap the parts that point to documentation - let docs = [ - for x in xs do - match x with - | FSharpToolTipElement.Group(ys) -> - for y in ys do + let docs = [ + for x in xs do + match x with + | FSharpToolTipElement.Group(ys) -> + for y in ys do yield y.XmlDoc | _ -> () ] // Render docs differently depending on how many overloads we find - match docs with - | [] -> return item - | [one] -> + match docs with + | [] -> return item + | [one] -> let value = docComment(one) let doc = Option.map markup value return {item with documentation=doc} - | many -> + | many -> let value = overloadComment(many) let doc = Option.map markup value return {item with documentation=doc} - } \ No newline at end of file + } diff --git a/src/LSP/LSP.fsproj b/src/LSP/LSP.fsproj index 6ddff7d..2bf8c77 100644 --- a/src/LSP/LSP.fsproj +++ b/src/LSP/LSP.fsproj @@ -14,6 +14,6 @@ - + - \ No newline at end of file + diff --git a/src/LSP/Ser.fs b/src/LSP/Ser.fs index 60288a5..206a7a5 100644 --- a/src/LSP/Ser.fs +++ b/src/LSP/Ser.fs @@ -8,32 +8,32 @@ open System.Text.RegularExpressions open FSharp.Data let private escapeChars = Regex("[\t\n\r\"\\\\]", RegexOptions.Compiled) -let private replaceChars = - MatchEvaluator(fun m -> - match m.Value with +let private replaceChars = + MatchEvaluator(fun m -> + match m.Value with | "\\" -> "\\\\" | "\t" -> "\\t" - | "\n" -> "\\n" - | "\r" -> "\\r" - | "\"" -> "\\\"" + | "\n" -> "\\n" + | "\r" -> "\\r" + | "\"" -> "\\\"" | v -> v) let private escapeStr(text:string) = let escaped = escapeChars.Replace(text, replaceChars) sprintf "\"%s\"" escaped -let private isOption(t: Type) = +let private isOption(t: Type) = t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<_ option> -let private isSeq(t: Type) = +let private isSeq(t: Type) = t.IsGenericType && t.GetGenericTypeDefinition() = typedefof> -let private implementsSeq(t: Type) = +let private implementsSeq(t: Type) = let is = t.GetInterfaces() Seq.exists isSeq is -let private isList(t: Type) = +let private isList(t: Type) = t.IsGenericType && t.GetGenericTypeDefinition() = typedefof> -let private isMap(t: Type) = +let private isMap(t: Type) = t.IsGenericType && t.GetGenericTypeDefinition() = typedefof> type JsonWriteOptions = { @@ -44,97 +44,97 @@ let defaultJsonWriteOptions: JsonWriteOptions = { customWriters = [] } -let private matchWriter(t: Type, w: obj): bool = - let domain, _ = w.GetType() |> FSharpType.GetFunctionElements +let private matchWriter(t: Type, w: obj): bool = + let domain, _ = w.GetType() |> FSharpType.GetFunctionElements domain.IsAssignableFrom(t) -let private findWriter(t: Type, customWriters: obj list): obj option = +let private findWriter(t: Type, customWriters: obj list): obj option = let matchT(w: obj) = matchWriter(t, w) - Seq.tryFind matchT customWriters + Seq.tryFind matchT customWriters -let asFun(w: obj): obj -> obj = +let asFun(w: obj): obj -> obj = let invoke = w.GetType().GetMethod("Invoke") fun x -> invoke.Invoke(w, [|x|]) -type MakeHelpers = - static member MakeList<'T> (items: obj seq): 'T list = +type MakeHelpers = + static member MakeList<'T> (items: obj seq): 'T list = [ for i in items do yield i :?> 'T ] - static member MakeMap<'T> (items: (string * obj) seq): Map = + static member MakeMap<'T> (items: (string * obj) seq): Map = let castV(k: string, v: obj) = (k, v :?> 'T) - let castItems = Seq.map castV items + let castItems = Seq.map castV items Map.ofSeq(castItems) - static member MakeOption<'T> (item: obj option): 'T option = - match item with - | None -> None + static member MakeOption<'T> (item: obj option): 'T option = + match item with + | None -> None | Some(i) -> Some(i :?> 'T) -let private makeList(t: Type, items: obj seq) = +let private makeList(t: Type, items: obj seq) = typeof.GetMethod("MakeList").MakeGenericMethod([|t|]).Invoke(null, [|items|]) -let private makeMap(t: Type, kvs: (string * obj) seq) = +let private makeMap(t: Type, kvs: (string * obj) seq) = typeof.GetMethod("MakeMap").MakeGenericMethod([|t|]).Invoke(null, [|kvs|]) -let private makeOption(t: Type, item: obj option) = +let private makeOption(t: Type, item: obj option) = typeof.GetMethod("MakeOption").MakeGenericMethod([|t|]).Invoke(null, [|item|]) -let rec private serializer (options: JsonWriteOptions, t: Type): obj -> string = +let rec private serializer (options: JsonWriteOptions, t: Type): obj -> string = let custom = findWriter(t, options.customWriters) - if custom.IsSome then + if custom.IsSome then let fObj = custom.Value let fType = fObj.GetType() let _, range = FSharpType.GetFunctionElements(fType) let serialize = serializer(options, range) let transform = asFun(fObj) fun o -> serialize(transform(o)) - elif t = typeof then + elif t = typeof then fun o -> sprintf "%b" (unbox o) - elif t = typeof then + elif t = typeof then fun o -> sprintf "%d" (unbox o) - elif t = typeof then + elif t = typeof then fun o -> sprintf "%c" (unbox o) |> escapeStr - elif t = typeof then + elif t = typeof then fun o -> escapeStr (o :?> string) - elif t = typeof then - fun o -> - let uri = o :?> Uri + elif t = typeof then + fun o -> + let uri = o :?> Uri escapeStr(uri.ToString()) - elif t = typeof then - fun o -> + elif t = typeof then + fun o -> let asJson = o :?> JsonValue asJson.ToString(JsonSaveOptions.DisableFormatting) - elif FSharpType.IsRecord t then + elif FSharpType.IsRecord t then let fields = FSharpType.GetRecordFields(t) let serializers = [|for f in fields do yield fieldSerializer(options, f)|] fun outer -> let fieldStrings = [|for f in serializers do yield f(outer)|] let innerString = String.concat "," fieldStrings sprintf "{%s}" innerString - elif implementsSeq t then - let [|innerType|] = t.GetGenericArguments() + elif implementsSeq t then + let innerType = t.GetGenericArguments().[0] let serializeInner = serializer(options, innerType) - fun outer -> + fun outer -> let asEnum = outer :?> System.Collections.IEnumerable let asSeq = Seq.cast(asEnum) - let inners = Seq.map serializeInner asSeq - let join = String.Join(',', inners) + let inners = Seq.map serializeInner asSeq + let join = String.Join(',', inners) sprintf "[%s]" join - elif isOption t then - let [|innerType|] = t.GetGenericArguments() + elif isOption t then + let innerType = t.GetGenericArguments().[0] let isSomeProp = t.GetProperty("IsSome") let isSome outer = isSomeProp.GetValue(None, [|outer|]) :?> bool let valueProp = t.GetProperty("Value") let serializeInner = serializer(options, innerType) fun outer -> - if isSome outer then + if isSome outer then let value = valueProp.GetValue outer serializeInner(value) else "null" - else + else raise (Exception (sprintf "Don't know how to serialize %s to JSON" (t.ToString()))) -and fieldSerializer (options: JsonWriteOptions, field: PropertyInfo): obj -> string = +and fieldSerializer (options: JsonWriteOptions, field: PropertyInfo): obj -> string = let name = escapeStr(field.Name) let innerSerializer = serializer(options, field.PropertyType) - fun outer -> + fun outer -> let value = field.GetValue(outer) let json = innerSerializer(value) sprintf "%s:%s" name json @@ -149,34 +149,34 @@ let defaultJsonReadOptions: JsonReadOptions = { customReaders = [] } -let private matchReader(t: Type, w: obj): bool = - let _, range = w.GetType() |> FSharpType.GetFunctionElements +let private matchReader(t: Type, w: obj): bool = + let _, range = w.GetType() |> FSharpType.GetFunctionElements t.IsAssignableFrom(range) -let private findReader(t: Type, customReaders: obj list): obj option = +let private findReader(t: Type, customReaders: obj list): obj option = let matchT(reader: obj) = matchReader(t, reader) - Seq.tryFind matchT customReaders + Seq.tryFind matchT customReaders -let rec private deserializer<'T> (options: JsonReadOptions, t: Type): JsonValue -> obj = +let rec private deserializer<'T> (options: JsonReadOptions, t: Type): JsonValue -> obj = let custom = findReader(t, options.customReaders) - if custom.IsSome then + if custom.IsSome then let domain, _ = FSharpType.GetFunctionElements(custom.Value.GetType()) let deserializeDomain = deserializer(options, domain) let deserializeInner = asFun(custom.Value) fun j -> deserializeInner(deserializeDomain(j)) - elif t = typeof then + elif t = typeof then fun j -> box(j.AsBoolean()) - elif t = typeof then + elif t = typeof then fun j -> box(j.AsInteger()) - elif t = typeof then + elif t = typeof then fun j -> let s = j.AsString() if s.Length = 1 then box(s.[0]) else raise(Exception(sprintf "Expected char but found '%s'" s)) - elif t = typeof then + elif t = typeof then fun j -> box(j.AsString()) - elif t = typeof then - fun j -> + elif t = typeof then + fun j -> // It seems that the Uri(_) constructor assumes the string has already been unescaped let escaped = j.AsString() let unescaped = Uri.UnescapeDataString(escaped) @@ -185,49 +185,51 @@ let rec private deserializer<'T> (options: JsonReadOptions, t: Type): JsonValue // It seems like either VSCode should be escaping # twice, or Uri(_) should be accepting escaped input let partlyEscaped = unescaped.Replace("?", "%3F").Replace("#", "%23") box(Uri(partlyEscaped)) - elif t = typeof then + elif t = typeof then fun j -> box(j) - elif isList t then - let [|innerType|] = t.GetGenericArguments() + elif isList t then + let innerType = t.GetGenericArguments().[0] let deserializeInner = deserializer(options, innerType) - fun j -> + fun j -> let array = j.AsArray() let parse = Seq.map deserializeInner array let list = makeList(innerType, parse) box(list) - elif isMap t then - let [|stringType; valueType|] = t.GetGenericArguments() + elif isMap t then + let genericArguments = t.GetGenericArguments() + let stringType = genericArguments.[0] + let valueType = genericArguments.[1] if stringType <> typeof then raise (Exception (sprintf "Keys of %A are not strings" t)) let deserializeInner = deserializer(options, valueType) - fun j -> + fun j -> let props = j.Properties() - let parse = Seq.map (fun (k, v) -> k, deserializeInner v) props - makeMap(valueType, parse) - elif isOption t then - let [|innerType|] = t.GetGenericArguments() + let parse = Seq.map (fun (k, v) -> k, deserializeInner v) props + makeMap(valueType, parse) + elif isOption t then + let innerType = t.GetGenericArguments().[0] let deserializeInner = deserializer(options, innerType) fun j -> - if j = JsonValue.Null then + if j = JsonValue.Null then box(makeOption(innerType, None)) - else + else let parse = deserializeInner j box(makeOption(innerType, Some parse)) - elif FSharpType.IsRecord t then + elif FSharpType.IsRecord t then let fields = FSharpType.GetRecordFields(t) let readers = [|for f in fields do yield fieldDeserializer(options, f)|] - fun j -> - let array = [| for field, reader in readers do + fun j -> + let array = [| for field, reader in readers do yield reader j |] FSharpValue.MakeRecord(t, array) - else + else raise (Exception (sprintf "Don't know how to deserialize %A from JSON" t)) -and fieldDeserializer (options: JsonReadOptions, field: PropertyInfo): string * (JsonValue -> obj) = +and fieldDeserializer (options: JsonReadOptions, field: PropertyInfo): string * (JsonValue -> obj) = let deserializeInner = deserializer(options, field.PropertyType) - let deserializeField(j: JsonValue) = + let deserializeField(j: JsonValue) = let value = match j.TryGetProperty(field.Name) with Some v -> v | None -> JsonValue.Null box(deserializeInner(value)) field.Name, deserializeField -let deserializerFactory<'T>(options: JsonReadOptions): JsonValue -> 'T = +let deserializerFactory<'T>(options: JsonReadOptions): JsonValue -> 'T = let d = deserializer(options, typeof<'T>) fun s -> d(s) :?> 'T diff --git a/src/ProjectCracker/ProjectCracker.fs b/src/ProjectCracker/ProjectCracker.fs index b0d038c..d0d0242 100644 --- a/src/ProjectCracker/ProjectCracker.fs +++ b/src/ProjectCracker/ProjectCracker.fs @@ -25,29 +25,29 @@ open Buildalyzer type CrackedProject = { /// ?.fsproj file that was cracked - fsproj: FileInfo + fsproj: FileInfo /// ?.dll file built by this .fsproj file /// Dependent projects will reference this dll in fscArgs, like "-r:?.dll" target: FileInfo /// List of source files. /// These are fsc args, but presented separately because that's how FSharpProjectOptions wants them. sources: FileInfo list - /// .fsproj files - projectReferences: FileInfo list + /// .fsproj files + projectReferences: FileInfo list /// .dlls corresponding to non-F# projects otherProjectReferences: FileInfo list /// .dlls - packageReferences: FileInfo list + packageReferences: FileInfo list /// .dlls referenced with - directReferences: FileInfo list + directReferences: FileInfo list /// An error was encountered while cracking the project /// This message should be displayed at the top of every file error: string option } /// Read the assembly name and version from a .dll using System.Reflection.Metadata -let private readAssembly(dll: FileInfo): Result = - try +let private readAssembly(dll: FileInfo): Result = + try use fileStream = new FileStream(dll.FullName, FileMode.Open, FileAccess.Read) use peReader = new PEReader(fileStream, PEStreamOptions.LeaveOpen) let metadataReader = peReader.GetMetadataReader() @@ -57,8 +57,8 @@ let private readAssembly(dll: FileInfo): Result = type private ProjectAssets = { projectName: string - framework: string - packages: FileInfo list + framework: string + packages: FileInfo list projects: FileInfo list } @@ -75,10 +75,10 @@ type private CoreRuntime = { } type JsonValue with - member x.GetCaseInsensitive(propertyName) = + member x.GetCaseInsensitive(propertyName) = let mutable result: Option = None - for (k, v) in x.Properties do - if String.Equals(propertyName, k, StringComparison.OrdinalIgnoreCase ) then + for (k, v) in x.Properties do + if String.Equals(propertyName, k, StringComparison.OrdinalIgnoreCase ) then result <- Some(v) result @@ -118,10 +118,10 @@ let private frameworkPreference = [ "net20", ".NETFramework,Version=v2.0"; "net11", ".NETFramework,Version=v1.1" ] -let private parseProjectAssets(projectAssetsJson: FileInfo): ProjectAssets = +let private parseProjectAssets(projectAssetsJson: FileInfo): ProjectAssets = dprintfn "Parsing %s" projectAssetsJson.FullName let root = JsonValue.Parse(File.ReadAllText(projectAssetsJson.FullName)) - let fsproj = FileInfo(root?project?restore?projectPath.AsString()) + let fsproj = FileInfo(root?project?restore?projectPath.AsString()) // Find the assembly base name let projectName = root?project?restore?projectName.AsString() // Parses a version string into a version tuple @@ -144,8 +144,8 @@ let private parseProjectAssets(projectAssetsJson: FileInfo): ProjectAssets = CreateNoWindow=true, UseShellExecute=false, RedirectStandardOutput=true) - dotnetProcess.Start() - dotnetProcess.WaitForExit(5000) + dotnetProcess.Start() |> ignore + dotnetProcess.WaitForExit(5000) |> ignore let stdoutRaw = dotnetProcess.StandardOutput.ReadToEnd() let stdout = stdoutRaw.Trim().Split('\n') let runtimePaths = HashSet() @@ -190,29 +190,29 @@ let private parseProjectAssets(projectAssetsJson: FileInfo): ProjectAssets = // Choose one of the frameworks listed in project.frameworks // by scanning all possible frameworks in order of preference - let shortFramework, longFramework = - let projectContainsFramework(short: string) = + let shortFramework, longFramework = + let projectContainsFramework(short: string) = root?project?frameworks.TryGetProperty(short).IsSome let mutable found: (string * string) option = None - for short, long in frameworkPreference do + for short, long in frameworkPreference do if projectContainsFramework(short) && found.IsNone then found <- Some(short, long) found.Value dprintfn "Chose framework %s / %s" shortFramework longFramework // Choose a version of a dependency by scanning targets - let chooseVersion(dependencyName: string): string = + let chooseVersion(dependencyName: string): string = let prefix = dependencyName + "/" - let mutable found: string option = None - for dependencyVersion, _ in root?targets.[longFramework].Properties do - if dependencyVersion.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && found.IsNone then + let mutable found: string option = None + for dependencyVersion, _ in root?targets.[longFramework].Properties do + if dependencyVersion.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && found.IsNone then let version = dependencyVersion.Substring(prefix.Length) found <- Some(version) - match found with + match found with | Some(d) -> d - | None -> + | None -> let keys = Array.map fst root?targets.[longFramework].Properties raise(Exception(sprintf "No version of %s found in %A" dependencyName keys)) - // All transitive dependencies of the project + // All transitive dependencies of the project // "FSharp.Core/4.3.4" => {FSharp.Core, 4.3.4, false} let transitiveDependencies = Dictionary() // Find transitive dependencies by scanning the targets section @@ -234,22 +234,22 @@ let private parseProjectAssets(projectAssetsJson: FileInfo): ProjectAssets = // }, // } // } - let rec findTransitiveDeps(parent: Dep) = + let rec findTransitiveDeps(parent: Dep) = let nameVersion = parent.name + "/" + parent.version - match root?targets.[longFramework].GetCaseInsensitive(nameVersion) with + match root?targets.[longFramework].GetCaseInsensitive(nameVersion) with | None -> dprintfn "Couldn't find %s in targets" nameVersion | Some (lib) when transitiveDependencies.TryAdd(nameVersion, parent) -> - match lib.TryGetProperty("dependencies") with + match lib.TryGetProperty("dependencies") with | None -> () - | Some(next) -> - for name, _ in next.Properties do - // The version in "dependencies" can be a range like [1.0, 2.0), + | Some(next) -> + for name, _ in next.Properties do + // The version in "dependencies" can be a range like [1.0, 2.0), // so we will use the version from targets let version = chooseVersion(name) let child = {name=name; version=version} let childNameVersion = child.name + "/" + child.version - if not(transitiveDependencies.ContainsKey(childNameVersion)) then + if not(transitiveDependencies.ContainsKey(childNameVersion)) then dprintfn "\t%s <- %s" nameVersion childNameVersion findTransitiveDeps(child) | _ -> () @@ -280,8 +280,8 @@ let private parseProjectAssets(projectAssetsJson: FileInfo): ProjectAssets = // } // } let autoReferenced = HashSet() - for name, dep in root?project?frameworks.[shortFramework]?dependencies.Properties do - if dep.TryGetProperty("autoReferenced") = Some(JsonValue.Boolean(true)) then + for name, dep in root?project?frameworks.[shortFramework]?dependencies.Properties do + if dep.TryGetProperty("autoReferenced") = Some(JsonValue.Boolean(true)) then autoReferenced.Add(name) |> ignore let version = chooseVersion(name) let dep = {name=name; version=version} @@ -318,7 +318,7 @@ let private parseProjectAssets(projectAssetsJson: FileInfo): ProjectAssets = selectedRuntimes.[runtimeKey] <- runtime else selectedRuntimes.[runtimeKey] <- runtime - | None -> + | None -> () // ["/Users/georgefraser/.nuget/packages/", ...] let packageFolders = [for p, _ in root?packageFolders.Properties do yield p] @@ -366,23 +366,23 @@ let private parseProjectAssets(projectAssetsJson: FileInfo): ProjectAssets = [ for runtimeDir in selectedRuntimes.Values do yield! Directory.GetFiles(runtimeDir.path, "*.dll")] dprintfn "Discovered runtime assemblies %A" runtimeAssemblies // Find projects in libraries section - for name, value in root?libraries.Properties do - if value?``type``.AsString() = "project" then + for name, value in root?libraries.Properties do + if value?``type``.AsString() = "project" then match name.Split('/') with - | [|name; version|] -> + | [|name; version|] -> let dep = {name=name; version=version} findTransitiveDeps(dep) - | _ -> + | _ -> dprintfn "%s doesn't look like name/version" name // Search package folders for a .dll - let absoluteDll(relativeToPackageFolder: string): string option = + let absoluteDll(relativeToPackageFolder: string): string option = let mutable found: string option = None - for packageFolder in packageFolders do + for packageFolder in packageFolders do let candidate = Path.Combine(packageFolder, relativeToPackageFolder) - if File.Exists(candidate) && found.IsNone then + if File.Exists(candidate) && found.IsNone then found <- Some(candidate) - if found.IsNone then - dprintfn "Couldn't find %s in %A" relativeToPackageFolder packageFolders + if found.IsNone then + dprintfn "Couldn't find %s in %A" relativeToPackageFolder packageFolders found // Find .dll files for each dependency // "targets": { @@ -408,54 +408,54 @@ let private parseProjectAssets(projectAssetsJson: FileInfo): ProjectAssets = // "files": [ ... ] // }, // } - let findDlls(dep: Dep) = + let findDlls(dep: Dep) = let nameVersion = dep.name + "/" + dep.version let lib = root?libraries.GetCaseInsensitive(nameVersion).Value let prefix = lib?path.AsString() // For autoReferenced=true dependencies, we will include all dlls - if autoReferenced.Contains(dep.name) then - [ for json in lib?files.AsArray() do + if autoReferenced.Contains(dep.name) then + [ for json in lib?files.AsArray() do let f = json.AsString() if f.EndsWith(".dll") then let relative = Path.Combine(prefix, f) - match absoluteDll(relative) with + match absoluteDll(relative) with | None -> () | Some(f) -> yield f ] // Otherwise, we'll look at the list of "compile" .dlls in "targets" - else + else let target = root?targets.[longFramework].GetCaseInsensitive(nameVersion).Value - match target.TryGetProperty("compile") with - | None -> + match target.TryGetProperty("compile") with + | None -> dprintfn "%s has no compile-time dependencies" nameVersion [] - | Some(map) -> - [ for dll, _ in map.Properties do + | Some(map) -> + [ for dll, _ in map.Properties do let rel = Path.Combine(prefix, dll) - match absoluteDll(rel) with + match absoluteDll(rel) with | None -> () | Some(abs) -> yield abs ] // Find all package dlls let packageDlls = [for d in transitiveDependencies.Values do yield! findDlls(d)] // Resolve conflicts by getting name and version from each DLL, choosing the highest version let packageDllsWithoutConflicts = - let dllNameVersion = - [ for d in packageDlls @ runtimeAssemblies do - match readAssembly(FileInfo(d)) with + let dllNameVersion = + [ for d in packageDlls @ runtimeAssemblies do + match readAssembly(FileInfo(d)) with | Error(e) -> dprintfn "Failed loading %s with error %s" d e | Ok(name, version) -> yield d, name, version ] let aName(dll, name, version) = name let aVersion(dll, name, version) = version let byName = List.groupBy aName dllNameVersion - [ for name, versions in byName do - match versions with + [ for name, versions in byName do + match versions with | [(file, _, _)] -> yield file - | _ -> + | _ -> let winner, _, _ = List.maxBy aVersion versions dprintfn "Conflict between %A, chose %s" versions winner yield winner ] // Find all transitive project dependencies by examining libraries // Values with "type": "project" are projects - // All transitive projects will already be included in project.assets.json + // All transitive projects will already be included in project.assets.json // "libraries": { // "LSP/1.0.0": { // "type": "project", @@ -463,9 +463,9 @@ let private parseProjectAssets(projectAssetsJson: FileInfo): ProjectAssets = // "msbuildProject": "../LSP/LSP.fsproj" // } // } - let projects = - [ for name, library in root?libraries.Properties do - if library?``type``.AsString() = "project" then + let projects = + [ for name, library in root?libraries.Properties do + if library?``type``.AsString() = "project" then let rel = library?path.AsString() let abs = Path.Combine(fsproj.DirectoryName, rel) let norm = Path.GetFullPath(abs) @@ -477,76 +477,76 @@ let private parseProjectAssets(projectAssetsJson: FileInfo): ProjectAssets = projects=projects } -let private project(fsproj: FileInfo): ProjectAnalyzer = +let private project(fsproj: FileInfo): ProjectAnalyzer = let options = new AnalyzerManagerOptions() options.LogWriter <- !diagnosticsLog // TODO this doesn't follow ref changes let manager = AnalyzerManager(options) manager.GetProject(fsproj.FullName) -let private inferTargetFramework(fsproj: FileInfo): AnalyzerResult = +let private inferTargetFramework(fsproj: FileInfo): AnalyzerResult = let builds = project(fsproj).Build() // TODO get target framework from project.assets.json let mutable chosen: AnalyzerResult option = None - for shortFramework, _ in frameworkPreference do - if chosen.IsNone then - for build in builds do - if build.TargetFramework = shortFramework then + for shortFramework, _ in frameworkPreference do + if chosen.IsNone then + for build in builds do + if build.TargetFramework = shortFramework then chosen <- Some(build) if chosen.IsNone then - for build in builds do - if chosen.IsNone then + for build in builds do + if chosen.IsNone then chosen <- Some(build) chosen.Value -let private projectTarget(csproj: FileInfo) = +let private projectTarget(csproj: FileInfo) = let baseName = Path.GetFileNameWithoutExtension(csproj.Name) let dllName = baseName + ".dll" let placeholderTarget = FileInfo(Path.Combine [|csproj.DirectoryName; "bin"; "Debug"; "placeholder"; dllName|]) let projectAssetsJson = FileInfo(Path.Combine [|csproj.DirectoryName; "obj"; "project.assets.json"|]) - if projectAssetsJson.Exists then + if projectAssetsJson.Exists then let assets = parseProjectAssets(projectAssetsJson) let dllName = assets.projectName + ".dll" // TODO this seems fragile FileInfo(Path.Combine [|csproj.DirectoryName; "bin"; "Debug"; assets.framework; dllName|]) - else + else placeholderTarget -let private absoluteIncludePath(fsproj: FileInfo, i: ProjectItem) = +let private absoluteIncludePath(fsproj: FileInfo, i: ProjectItem) = let relativePath = i.ItemSpec.Replace('\\', Path.DirectorySeparatorChar) let absolutePath = Path.Combine(fsproj.DirectoryName, relativePath) let normalizePath = Path.GetFullPath(absolutePath) FileInfo(normalizePath) /// Crack an .fsproj file by: -/// - Running the "Restore" target and reading +/// - Running the "Restore" target and reading /// - Reading .fsproj using the MSBuild API /// - Reading libraries from project.assets.json -let crack(fsproj: FileInfo): CrackedProject = +let crack(fsproj: FileInfo): CrackedProject = // Figure out name of output .dll let baseName = Path.GetFileNameWithoutExtension(fsproj.Name) let dllName = baseName + ".dll" let placeholderTarget = FileInfo(Path.Combine [|fsproj.DirectoryName; "bin"; "Debug"; "placeholder"; dllName|]) - try + try // Get source info from .fsproj let timeProject = Stopwatch.StartNew() let project = inferTargetFramework(fsproj) - let sources = - [ for KeyValue(k, v) in project.Items do - if k = "Compile" then - for i in v do + let sources = + [ for KeyValue(k, v) in project.Items do + if k = "Compile" then + for i in v do yield absoluteIncludePath(fsproj, i) ] - let directReferences = - [ for KeyValue(k, v) in project.Items do - if k = "Reference" then - for i in v do + let directReferences = + [ for KeyValue(k, v) in project.Items do + if k = "Reference" then + for i in v do yield absoluteIncludePath(fsproj, i) ] dprintfn "Cracked %s in %dms" fsproj.Name timeProject.ElapsedMilliseconds // Get package info from project.assets.json let projectAssetsJson = FileInfo(Path.Combine [|fsproj.DirectoryName; "obj"; "project.assets.json"|]) if not(projectAssetsJson.Exists) then { - fsproj=fsproj - target=placeholderTarget + fsproj=fsproj + target=placeholderTarget sources=sources projectReferences=[] otherProjectReferences=[] @@ -574,7 +574,7 @@ let crack(fsproj: FileInfo): CrackedProject = directReferences=directReferences error=None } - with e -> + with e -> dprintfn "Failed to build %s: %s\n%s" fsproj.Name e.Message e.StackTrace { fsproj=fsproj