diff --git a/news/1 Enhancements/4520.md b/news/1 Enhancements/4520.md new file mode 100644 index 000000000000..8c2228bc806a --- /dev/null +++ b/news/1 Enhancements/4520.md @@ -0,0 +1 @@ +Add support for palette commands to live share scenario. diff --git a/package-lock.json b/package-lock.json index fd2c8b7fcc37..37c4c9c8cc11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2330,7 +2330,8 @@ "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true }, "arr-filter": { "version": "1.1.2", @@ -2344,7 +2345,8 @@ "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true }, "arr-map": { "version": "2.0.2", @@ -2358,7 +2360,8 @@ "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true }, "array-differ": { "version": "1.0.0", @@ -2480,7 +2483,8 @@ "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true }, "array.prototype.flat": { "version": "1.2.1", @@ -2561,7 +2565,8 @@ "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true }, "async": { "version": "1.5.2", @@ -2610,7 +2615,8 @@ "atob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "dev": true }, "awesome-typescript-loader": { "version": "5.2.1", @@ -2975,6 +2981,7 @@ "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, "requires": { "cache-base": "^1.0.1", "class-utils": "^0.3.5", @@ -2989,6 +2996,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -2997,6 +3005,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -3005,6 +3014,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -3013,6 +3023,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -3189,6 +3200,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -3206,6 +3218,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -3429,6 +3442,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, "requires": { "collection-visit": "^1.0.0", "component-emitter": "^1.2.1", @@ -3704,6 +3718,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, "requires": { "arr-union": "^3.1.0", "define-property": "^0.2.5", @@ -3715,6 +3730,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -3932,6 +3948,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, "requires": { "map-visit": "^1.0.0", "object-visit": "^1.0.0" @@ -4007,7 +4024,8 @@ "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true }, "concat-map": { "version": "0.0.1", @@ -4130,7 +4148,8 @@ "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true }, "copy-props": { "version": "2.0.4", @@ -4780,6 +4799,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -4824,7 +4844,8 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true }, "decompress": { "version": "4.2.0", @@ -5021,6 +5042,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, "requires": { "is-descriptor": "^1.0.2", "isobject": "^3.0.1" @@ -5030,6 +5052,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -5038,6 +5061,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -5046,6 +5070,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -5822,6 +5847,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, "requires": { "debug": "^2.3.3", "define-property": "^0.2.5", @@ -5836,6 +5862,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -5844,6 +5871,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -5976,6 +6004,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -5985,6 +6014,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -6017,6 +6047,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, "requires": { "array-unique": "^0.3.2", "define-property": "^1.0.0", @@ -6032,6 +6063,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -6040,6 +6072,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -6048,6 +6081,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -6056,6 +6090,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -6064,6 +6099,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -6179,14 +6215,6 @@ "schema-utils": "^1.0.0" } }, - "file-matcher": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/file-matcher/-/file-matcher-1.3.0.tgz", - "integrity": "sha512-3CYUK4tsa+ssJZc0mzGF8AAh+8uMAFhiHMw9thRejqEMhTTuusC9UPTDA/NVn4msdXTC1b66HQq55VCh7CuZog==", - "requires": { - "micromatch": "^3.1.10" - } - }, "file-type": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/file-type/-/file-type-7.7.1.tgz", @@ -6226,6 +6254,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -6237,6 +6266,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -6338,7 +6368,8 @@ "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true }, "for-own": { "version": "1.0.0", @@ -6380,6 +6411,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, "requires": { "map-cache": "^0.2.2" } @@ -7056,7 +7088,8 @@ "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true }, "getpass": { "version": "0.1.7", @@ -8382,6 +8415,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, "requires": { "get-value": "^2.0.6", "has-values": "^1.0.0", @@ -8392,6 +8426,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, "requires": { "is-number": "^3.0.0", "kind-of": "^4.0.0" @@ -8401,6 +8436,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -9001,6 +9037,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -9009,6 +9046,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -9085,6 +9123,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -9093,6 +9132,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -9115,6 +9155,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, "requires": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -9124,7 +9165,8 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true } } }, @@ -9152,7 +9194,8 @@ "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true }, "is-extglob": { "version": "2.1.1", @@ -9197,6 +9240,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -9205,6 +9249,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -9233,6 +9278,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "dev": true, "requires": { "is-number": "^4.0.0" }, @@ -9240,7 +9286,8 @@ "is-number": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true } } }, @@ -9278,6 +9325,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "requires": { "isobject": "^3.0.1" } @@ -9397,7 +9445,8 @@ "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true }, "is-word-character": { "version": "1.0.2", @@ -9425,7 +9474,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "isomorphic-fetch": { "version": "2.2.1", @@ -9911,7 +9961,8 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true }, "labella": { "version": "1.1.4", @@ -10411,7 +10462,8 @@ "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true }, "map-stream": { "version": "0.1.0", @@ -10423,6 +10475,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, "requires": { "object-visit": "^1.0.0" } @@ -10566,6 +10619,7 @@ "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -10690,6 +10744,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, "requires": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" @@ -10699,6 +10754,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -10825,7 +10881,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "multimatch": { "version": "2.1.0", @@ -10876,6 +10933,7 @@ "version": "1.2.9", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "dev": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -12344,6 +12402,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, "requires": { "copy-descriptor": "^0.1.0", "define-property": "^0.2.5", @@ -12354,6 +12413,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -12362,6 +12422,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -12390,6 +12451,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, "requires": { "isobject": "^3.0.0" } @@ -12475,6 +12537,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, "requires": { "isobject": "^3.0.1" } @@ -12865,7 +12928,8 @@ "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true }, "path-browserify": { "version": "0.0.0", @@ -13120,7 +13184,8 @@ "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true }, "postcss": { "version": "6.0.23", @@ -13909,6 +13974,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, "requires": { "extend-shallow": "^3.0.2", "safe-regex": "^1.1.0" @@ -14131,12 +14197,14 @@ "repeat-element": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true }, "replace-ext": { "version": "0.0.1", @@ -14273,7 +14341,8 @@ "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true }, "responselike": { "version": "1.0.2", @@ -14297,7 +14366,8 @@ "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true }, "retyped-diff-match-patch-tsd-ambient": { "version": "1.0.0-1", @@ -14446,6 +14516,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, "requires": { "ret": "~0.1.10" } @@ -14683,6 +14754,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -14694,6 +14766,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -14789,6 +14862,7 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, "requires": { "base": "^0.11.1", "debug": "^2.2.0", @@ -14804,6 +14878,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -14812,6 +14887,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -14822,6 +14898,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, "requires": { "define-property": "^1.0.0", "isobject": "^3.0.0", @@ -14832,6 +14909,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -14840,6 +14918,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -14848,6 +14927,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -14856,6 +14936,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -14868,6 +14949,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, "requires": { "kind-of": "^3.2.0" }, @@ -14876,6 +14958,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -14923,12 +15006,14 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true }, "source-map-resolve": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, "requires": { "atob": "^2.1.1", "decode-uri-component": "^0.2.0", @@ -14958,7 +15043,8 @@ "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true }, "sparkles": { "version": "1.0.1", @@ -15011,6 +15097,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, "requires": { "extend-shallow": "^3.0.0" } @@ -15066,6 +15153,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, "requires": { "define-property": "^0.2.5", "object-copy": "^0.1.0" @@ -15075,6 +15163,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -15735,6 +15824,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -15743,6 +15833,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -15753,6 +15844,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, "requires": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", @@ -15764,6 +15856,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -16500,6 +16593,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", @@ -16511,6 +16605,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -16519,6 +16614,7 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -16624,6 +16720,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, "requires": { "has-value": "^0.3.1", "isobject": "^3.0.0" @@ -16633,6 +16730,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, "requires": { "get-value": "^2.0.3", "has-values": "^0.1.4", @@ -16643,6 +16741,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, "requires": { "isarray": "1.0.0" } @@ -16652,7 +16751,8 @@ "has-values": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true } } }, @@ -16693,7 +16793,8 @@ "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true }, "url": { "version": "0.11.0", @@ -16758,6 +16859,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "dev": true, "requires": { "kind-of": "^6.0.2" } diff --git a/package.json b/package.json index 2974f2874691..60e4019dfd7f 100644 --- a/package.json +++ b/package.json @@ -2172,7 +2172,6 @@ "arch": "^2.1.0", "azure-storage": "^2.10.1", "diff-match-patch": "^1.0.0", - "file-matcher": "^1.3.0", "fs-extra": "^4.0.3", "fuzzy": "^0.1.3", "get-port": "^3.2.0", diff --git a/src/client/datascience/codeCssGenerator.ts b/src/client/datascience/codeCssGenerator.ts index e2d712ed88fa..63da744d1d6c 100644 --- a/src/client/datascience/codeCssGenerator.ts +++ b/src/client/datascience/codeCssGenerator.ts @@ -2,17 +2,16 @@ // Licensed under the MIT License. 'use strict'; import { JSONArray, JSONObject, JSONValue } from '@phosphor/coreutils'; -import { FindOptions } from 'file-matcher'; import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as path from 'path'; import * as stripJsonComments from 'strip-json-comments'; import { IWorkspaceService } from '../common/application/types'; -import { ICurrentProcess, ILogger } from '../common/types'; +import { ILogger } from '../common/types'; import { EXTENSION_ROOT_DIR } from '../constants'; import { Identifiers } from './constants'; -import { ICodeCssGenerator } from './types'; +import { ICodeCssGenerator, IThemeFinder } from './types'; // tslint:disable:no-any @@ -26,7 +25,7 @@ import { ICodeCssGenerator } from './types'; export class CodeCssGenerator implements ICodeCssGenerator { constructor( @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(ICurrentProcess) private currentProcess: ICurrentProcess, + @inject(IThemeFinder) private themeFinder: IThemeFinder, @inject(ILogger) private logger: ILogger) { } @@ -42,10 +41,12 @@ export class CodeCssGenerator implements ICodeCssGenerator { // Then we have to find where the theme resources are loaded from if (theme) { + this.logger.logInformation('Searching for token colors ...'); const tokenColors = await this.findTokenColors(theme); // The tokens object then contains the necessary data to generate our css if (tokenColors && font && fontSize) { + this.logger.logInformation('Using colors to generate CSS ...'); return this.generateCss(theme, tokenColors, font, fontSize, terminalCursor); } } @@ -57,20 +58,15 @@ export class CodeCssGenerator implements ICodeCssGenerator { return ''; } - private escapeThemeName(themeName: string) : string { - return themeName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - } - private matchTokenColor(tokenColors: JSONArray, scope: string) : number { return tokenColors.findIndex((entry: any) => { if (entry) { const scopes = entry['scope'] as JSONValue; - if (scopes && Array.isArray(scopes)) { - if (scopes.find(v => v !== null && v !== undefined && v.toString() === scope)) { + if (scopes) { + const scopeArray = Array.isArray(scope) ? scopes as JSONArray : scopes.toString().split(','); + if (scopeArray.find(v => v !== null && v !== undefined && v.toString().trim() === scope)) { return true; } - } else if (scopes && scopes.toString() === scope) { - return true; } } @@ -78,7 +74,7 @@ export class CodeCssGenerator implements ICodeCssGenerator { }); } - private getScopeColor = (tokenColors: JSONArray, scope: string, secondary?: string): string => { + private getScopeStyle = (tokenColors: JSONArray, scope: string, secondary?: string): { color: string; fontStyle: string } => { // Search through the scopes on the json object let match = this.matchTokenColor(tokenColors, scope); if (match < 0 && secondary) { @@ -88,12 +84,15 @@ export class CodeCssGenerator implements ICodeCssGenerator { if (found !== null) { const settings = found['settings']; if (settings && settings !== null) { - return settings['foreground']; + const fontStyle = settings['fontStyle'] ? settings['fontStyle'] : 'normal'; + const foreground = settings['foreground'] ? settings['foreground'] : 'var(--vscode-editor-foreground)'; + + return { fontStyle, color: foreground }; } } // Default to editor foreground - return 'var(--vscode-editor-foreground)'; + return { color: 'var(--vscode-editor-foreground)', fontStyle: 'normal' }; } // tslint:disable-next-line:max-func-body-length @@ -101,15 +100,15 @@ export class CodeCssGenerator implements ICodeCssGenerator { const escapedThemeName = Identifiers.GeneratedThemeName; // There's a set of values that need to be found - const comment = this.getScopeColor(tokenColors, 'comment'); - const numeric = this.getScopeColor(tokenColors, 'constant.numeric'); - const stringColor = this.getScopeColor(tokenColors, 'string'); - const keyword = this.getScopeColor(tokenColors, 'keyword.control', 'keyword'); - const operator = this.getScopeColor(tokenColors, 'keyword.operator'); - const variable = this.getScopeColor(tokenColors, 'variable'); + const commentStyle = this.getScopeStyle(tokenColors, 'comment'); + const numericStyle = this.getScopeStyle(tokenColors, 'constant.numeric'); + const stringStyle = this.getScopeStyle(tokenColors, 'string'); + const keywordStyle = this.getScopeStyle(tokenColors, 'keyword.control', 'keyword'); + const operatorStyle = this.getScopeStyle(tokenColors, 'keyword.operator'); + const variableStyle = this.getScopeStyle(tokenColors, 'variable'); // const atomic = this.getScopeColor(tokenColors, 'atomic'); - const builtin = this.getScopeColor(tokenColors, 'support.function'); - const punctuation = this.getScopeColor(tokenColors, 'punctuation'); + const builtinStyle = this.getScopeStyle(tokenColors, 'support.function'); + const punctuationStyle = this.getScopeStyle(tokenColors, 'punctuation'); const def = 'var(--vscode-editor-foreground)'; @@ -122,7 +121,7 @@ export class CodeCssGenerator implements ICodeCssGenerator { // Use these values to fill in our format string return ` :root { - --code-comment-color: ${comment}; + --code-comment-color: ${commentStyle.color}; --code-font-family: ${fontFamily}; --code-font-size:${fontSize}px; } @@ -131,19 +130,19 @@ export class CodeCssGenerator implements ICodeCssGenerator { .cm-link {text-decoration: underline;} .cm-strikethrough {text-decoration: line-through;} - .cm-s-${escapedThemeName} span.cm-keyword {color: ${keyword};} - .cm-s-${escapedThemeName} span.cm-number {color: ${numeric};} - .cm-s-${escapedThemeName} span.cm-def {color: ${def};} - .cm-s-${escapedThemeName} span.cm-variable {color: ${variable};} - .cm-s-${escapedThemeName} span.cm-punctuation {color: ${punctuation};} + .cm-s-${escapedThemeName} span.cm-keyword {color: ${keywordStyle.color}; font-style: ${keywordStyle.fontStyle}; } + .cm-s-${escapedThemeName} span.cm-number {color: ${numericStyle.color}; font-style: ${numericStyle.fontStyle}; } + .cm-s-${escapedThemeName} span.cm-def {color: ${def}; } + .cm-s-${escapedThemeName} span.cm-variable {color: ${variableStyle.color}; font-style: ${variableStyle.fontStyle}; } + .cm-s-${escapedThemeName} span.cm-punctuation {color: ${punctuationStyle.color}; font-style: ${punctuationStyle.fontStyle}; } .cm-s-${escapedThemeName} span.cm-property, - .cm-s-${escapedThemeName} span.cm-operator {color: ${operator};} - .cm-s-${escapedThemeName} span.cm-variable-2 {color: ${variable};} - .cm-s-${escapedThemeName} span.cm-variable-3, .cm-s-${theme} .cm-type {color: ${variable};} - .cm-s-${escapedThemeName} span.cm-comment {color: ${comment};} - .cm-s-${escapedThemeName} span.cm-string {color: ${stringColor};} - .cm-s-${escapedThemeName} span.cm-string-2 {color: ${stringColor};} - .cm-s-${escapedThemeName} span.cm-builtin {color: ${builtin};} + .cm-s-${escapedThemeName} span.cm-operator {color: ${operatorStyle.color}; font-style: ${operatorStyle.fontStyle}; } + .cm-s-${escapedThemeName} span.cm-variable-2 {color: ${variableStyle.color}; font-style: ${variableStyle.fontStyle}; } + .cm-s-${escapedThemeName} span.cm-variable-3, .cm-s-${theme} .cm-type {color: ${variableStyle.color}; font-style: ${variableStyle.fontStyle}; } + .cm-s-${escapedThemeName} span.cm-comment {color: ${commentStyle.color}; font-style: ${commentStyle.fontStyle}; } + .cm-s-${escapedThemeName} span.cm-string {color: ${stringStyle.color}; font-style: ${stringStyle.fontStyle}; } + .cm-s-${escapedThemeName} span.cm-string-2 {color: ${stringStyle.color}; font-style: ${stringStyle.fontStyle}; } + .cm-s-${escapedThemeName} span.cm-builtin {color: ${builtinStyle.color}; font-style: ${builtinStyle.fontStyle}; } .cm-s-${escapedThemeName} div.CodeMirror-cursor ${cursorStyle} .cm-s-${escapedThemeName} div.CodeMirror-selected {background: var(--vscode-editor-selectionBackground) !important;} `; @@ -171,42 +170,27 @@ export class CodeCssGenerator implements ICodeCssGenerator { return tokenColors; } + // Might also have a 'settings' object that equates to token colors + const settings = theme['settings'] as JSONArray; + if (settings && settings.length > 0) { + return settings; + } + return []; } private findTokenColors = async (theme: string): Promise => { - const currentExe = this.currentProcess.execPath; - let currentPath = path.dirname(currentExe); - - // Should be somewhere under currentPath/resources/app/extensions inside of a json file - let extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions'); - if (!(await fs.pathExists(extensionsPath))) { - // Might be on mac or linux. try a different path - currentPath = path.resolve(currentPath, '../../../..'); - extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions'); - } - - // Search through all of the json files for the theme name - const escapedThemeName = this.escapeThemeName(theme); - const searchOptions: FindOptions = { - path: extensionsPath, - recursiveSearch: true, - fileFilter: { - fileNamePattern: '**/*.json', - content: new RegExp(`[name|id][',"]:\\s*[',"]${escapedThemeName}[',"]`) - } - }; - // tslint:disable-next-line:no-require-imports - const fm = require('file-matcher') as typeof import('file-matcher'); - const matcher = new fm.FileMatcher(); try { - const results = await matcher.find(searchOptions); + this.logger.logInformation('Attempting search for colors ...'); + const themeRoot = await this.themeFinder.findThemeRootJson(theme); // Use the first result if we have one - if (results && results.length > 0) { + if (themeRoot) { + this.logger.logInformation(`Loading colors from ${themeRoot} ...`); + // This should be the path to the file. Load it as a json object - const contents = await fs.readFile(results[0], 'utf8'); + const contents = await fs.readFile(themeRoot, 'utf8'); const json = JSON.parse(stripJsonComments(contents)) as JSONObject; // There should be a theme colors section @@ -217,7 +201,7 @@ export class CodeCssGenerator implements ICodeCssGenerator { if (!contributes) { const tokenColors = json['tokenColors'] as JSONObject; if (tokenColors) { - return await this.readTokenColors(results[0]); + return await this.readTokenColors(themeRoot); } } @@ -226,16 +210,19 @@ export class CodeCssGenerator implements ICodeCssGenerator { // One of these (it's an array), should have our matching theme entry const index = themes.findIndex((e: any) => { - return e !== null && e['id'] === theme; + return e !== null && (e['id'] === theme || e['name'] === theme); }); const found = index >= 0 ? themes[index] as any : null; if (found !== null) { // Then the path entry should contain a relative path to the json file with // the tokens in it - const themeFile = path.join(path.dirname(results[0]), found['path']); + const themeFile = path.join(path.dirname(themeRoot), found['path']); + this.logger.logInformation(`Reading colors from ${themeFile}`); return await this.readTokenColors(themeFile); } + } else { + this.logger.logWarning(`Color theme ${theme} not found. Using default colors.`); } } catch (err) { // Swallow any exceptions with searching or parsing diff --git a/src/client/datascience/commandBroker.ts b/src/client/datascience/commandBroker.ts deleted file mode 100644 index 1c9dddd777af..000000000000 --- a/src/client/datascience/commandBroker.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import { Disposable, TextEditor, TextEditorEdit } from 'vscode'; -import * as vsls from 'vsls/vscode'; - -import { ICommandManager, ILiveShareApi } from '../common/application/types'; -import { LiveShare } from './constants'; -import { PostOffice } from './liveshare/postOffice'; -import { ICommandBroker } from './types'; - -// tslint:disable:no-any - -// This class acts as a broker between the VSCode command manager and a potential live share session -// It works like so: -// -- If not connected to any live share session, then just register commands as normal -// -- If a host, register commands as normal (as they will be listened to), but when they are hit, post them to all guests -// -- If a guest, register commands as normal (as they will be ignored), but also register for notifications from the host. -@injectable() -export class CommandBroker implements ICommandBroker { - - private postOffice : PostOffice; - constructor( - @inject(ILiveShareApi) liveShare: ILiveShareApi, - @inject(ICommandManager) private commandManager: ICommandManager) { - this.postOffice = new PostOffice(LiveShare.CommandBrokerService, liveShare); - } - - public registerCommand(command: string, callback: (...args: any[]) => void, thisArg?: any): Disposable { - // Modify the callback such that it sends the command to our service - const disposable = this.commandManager.registerCommand(command, (...args: any[]) => this.wrapCallback(command, callback, ...args), thisArg); - - // Register it for lookup - this.register(command, callback, thisArg).ignoreErrors(); - - return disposable; - } - public registerTextEditorCommand(command: string, callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, thisArg?: any): Disposable { - // Modify the callback such that it sends the command to our service - const disposable = this.commandManager.registerCommand( - command, - (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => this.wrapTextEditorCallback(command, callback, textEditor, edit, ...args), thisArg); - - // Register it for lookup - this.register(command, callback, thisArg).ignoreErrors(); - - return disposable; - } - public executeCommand(command: string, ...rest: any[]): Thenable { - // Execute the command but potentially also send to our service too - this.postCommand(command, ...rest).ignoreErrors(); - return this.commandManager.executeCommand(command, ...rest); - } - public getCommands(filterInternal?: boolean): Thenable { - // This does not go across to the other side. Just return the command registered locally - return this.commandManager.getCommands(filterInternal); - } - - private async register(command: string, callback: (...args: any[]) => void, thisArg?: any) : Promise { - return this.postOffice.registerCallback(command, callback, thisArg); - } - - private wrapCallback(command: string, callback: (...args: any[]) => void, ...args: any[]) { - // Have the post office handle it. - this.postCommand(command, ...args).ignoreErrors(); - } - - private wrapTextEditorCallback(command: string, callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, ...args: any[]) { - // Not really supported at the moment as we don't have a special case for the textEditor. But not using it. - this.postCommand(command, ...args).ignoreErrors(); - } - - private async postCommand(command: string, ...rest: any[]): Promise { - // Make sure we're the host (or none). Guest shouldn't be sending - if (this.postOffice.role() !== vsls.Role.Guest) { - // This means we should send this across to the other side. - return this.postOffice.postCommand(command, ...rest); - } - } -} diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 1a2bb4c81405..c1fa8243c5ef 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -49,33 +49,6 @@ export namespace RegExpValues { export const ArgsSplitterRegEx = /([^\s,]+)/g; } -export namespace HistoryMessages { - export const StartCell = 'start_cell'; - export const FinishCell = 'finish_cell'; - export const UpdateCell = 'update_cell'; - export const GotoCodeCell = 'gotocell_code'; - export const RestartKernel = 'restart_kernel'; - export const Export = 'export_to_ipynb'; - export const GetAllCells = 'get_all_cells'; - export const ReturnAllCells = 'return_all_cells'; - export const DeleteCell = 'delete_cell'; - export const DeleteAllCells = 'delete_all_cells'; - export const Undo = 'undo'; - export const Redo = 'redo'; - export const ExpandAll = 'expand_all'; - export const CollapseAll = 'collapse_all'; - export const StartProgress = 'start_progress'; - export const StopProgress = 'stop_progress'; - export const Interrupt = 'interrupt'; - export const SubmitNewCell = 'submit_new_cell'; - export const UpdateSettings = 'update_settings'; -} - -export namespace HistoryNonLiveShareMessages { - export const SendInfo = 'send_info'; - export const Started = 'started'; -} - export enum Telemetry { ImportNotebook = 'DATASCIENCE.IMPORT_NOTEBOOK', RunCell = 'DATASCIENCE.RUN_CELL', @@ -103,7 +76,8 @@ export enum Telemetry { SubmitCellThroughInput = 'DATASCIENCE.SUBMITCELLFROMREPL', ConnectLocalJupyter = 'DATASCIENCE.CONNECTLOCALJUPYTER', ConnectRemoteJupyter = 'DATASCIENCE.CONNECTREMOTEJUPYTER', - ConnectFailedJupyter = 'DATASCIENCE.CONNECTFAILEDJUPYTER' + ConnectFailedJupyter = 'DATASCIENCE.CONNECTFAILEDJUPYTER', + RemoteAddCode = 'DATASCIENCE.LIVESHARE.ADDCODE' } export namespace HelpLinks { @@ -121,6 +95,7 @@ export namespace CodeSnippits { export namespace Identifiers { export const EmptyFileName = '2DB9B899-6519-4E1B-88B0-FA728A274115'; export const GeneratedThemeName = 'ipython-theme'; // This needs to be all lower class and a valid class name. + export const HistoryPurpose = 'history'; } export namespace JupyterCommands { @@ -132,16 +107,15 @@ export namespace JupyterCommands { } export namespace LiveShare { - export const None = 'none'; - export const Host = 'host'; - export const Guest = 'guest'; export const JupyterExecutionService = 'jupyterExecutionService'; export const JupyterServerSharedService = 'jupyterServerSharedService'; export const CommandBrokerService = 'commmandBrokerService'; export const WebPanelMessageService = 'webPanelMessageService'; + export const HistoryProviderService = 'historyProviderService'; export const LiveShareBroadcastRequest = 'broadcastRequest'; export const ResponseLifetime = 15000; export const ResponseRange = 1000; // Range of time alloted to check if a response matches or not + export const InterruptDefaultTimeout = 10000; } export namespace LiveShareCommands { @@ -151,8 +125,14 @@ export namespace LiveShareCommands { export const isKernelSpecSupported = 'isKernelSpecSupported'; export const connectToNotebookServer = 'connectToNotebookServer'; export const getUsableJupyterPython = 'getUsableJupyterPython'; + export const executeObservable = 'executeObservable'; export const getSysInfo = 'getSysInfo'; export const serverResponse = 'serverResponse'; export const catchupRequest = 'catchupRequest'; export const syncRequest = 'synchRequest'; + export const restart = 'restart'; + export const interrupt = 'interrupt'; + export const historyCreate = 'historyCreate'; + export const historyCreateSync = 'historyCreateSync'; + export const disposeServer = 'disposeServer'; } diff --git a/src/client/datascience/datascience.ts b/src/client/datascience/datascience.ts index f19fa5023ffb..5d866fd406b0 100644 --- a/src/client/datascience/datascience.ts +++ b/src/client/datascience/datascience.ts @@ -7,7 +7,7 @@ import { inject, injectable } from 'inversify'; import { URL } from 'url'; import * as vscode from 'vscode'; -import { IApplicationShell, IDocumentManager } from '../common/application/types'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; import { PYTHON_ALLFILES, PYTHON_LANGUAGE } from '../common/constants'; import { ContextKey } from '../common/contextKey'; import { @@ -23,13 +23,7 @@ import { IServiceContainer } from '../ioc/types'; import { captureTelemetry } from '../telemetry'; import { hasCells } from './cellFactory'; import { Commands, EditorContexts, Settings, Telemetry } from './constants'; -import { - ICodeWatcher, - ICommandBroker, - IDataScience, - IDataScienceCodeLensProvider, - IDataScienceCommandListener -} from './types'; +import { ICodeWatcher, IDataScience, IDataScienceCodeLensProvider, IDataScienceCommandListener } from './types'; @injectable() export class DataScience implements IDataScience { @@ -39,7 +33,7 @@ export class DataScience implements IDataScience { private changeHandler: IDisposable | undefined; private startTime: number = Date.now(); constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(ICommandBroker) private commandBroker: ICommandBroker, + @inject(ICommandManager) private commandManager: ICommandManager, @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, @inject(IExtensionContext) private extensionContext: IExtensionContext, @inject(IDataScienceCodeLensProvider) private dataScienceCodeLensProvider: IDataScienceCodeLensProvider, @@ -80,7 +74,7 @@ export class DataScience implements IDataScience { } } - public async runAllCells(file: string, id: string): Promise { + public async runAllCells(file: string): Promise { this.dataScienceSurveyBanner.showBanner().ignoreErrors(); let codeWatcher = this.getCodeWatcher(file); @@ -88,7 +82,7 @@ export class DataScience implements IDataScience { codeWatcher = this.getCurrentCodeWatcher(); } if (codeWatcher) { - return codeWatcher.runAllCells(id); + return codeWatcher.runAllCells(); } else { return Promise.resolve(); } @@ -96,44 +90,43 @@ export class DataScience implements IDataScience { // Note: see codewatcher.ts where the runcell command args are attached. The reason we don't have any // objects for parameters is because they can't be recreated when passing them through the LiveShare API - public async runCell(file: string, startLine: number, startChar: number, endLine: number, endChar: number, id: string): Promise { + public async runCell(file: string, startLine: number, startChar: number, endLine: number, endChar: number): Promise { this.dataScienceSurveyBanner.showBanner().ignoreErrors(); const codeWatcher = this.getCodeWatcher(file); if (codeWatcher) { - return codeWatcher.runCell(new vscode.Range(startLine, startChar, endLine, endChar), id); - } else { - return this.runCurrentCell(id); + return codeWatcher.runCell(new vscode.Range(startLine, startChar, endLine, endChar)); } } - public async runCurrentCell(id: string): Promise { + public async runCurrentCell(): Promise { this.dataScienceSurveyBanner.showBanner().ignoreErrors(); const activeCodeWatcher = this.getCurrentCodeWatcher(); if (activeCodeWatcher) { - return activeCodeWatcher.runCurrentCell(id); + return activeCodeWatcher.runCurrentCell(); } else { return Promise.resolve(); } } - public async runCurrentCellAndAdvance(id: string): Promise { + public async runCurrentCellAndAdvance(): Promise { this.dataScienceSurveyBanner.showBanner().ignoreErrors(); const activeCodeWatcher = this.getCurrentCodeWatcher(); if (activeCodeWatcher) { - return activeCodeWatcher.runCurrentCellAndAdvance(id); + return activeCodeWatcher.runCurrentCellAndAdvance(); } else { return Promise.resolve(); } } - public async runSelectionOrLine(id: string): Promise { + // tslint:disable-next-line:no-any + public async runSelectionOrLine(): Promise { this.dataScienceSurveyBanner.showBanner().ignoreErrors(); const activeCodeWatcher = this.getCurrentCodeWatcher(); if (activeCodeWatcher) { - return activeCodeWatcher.runSelectionOrLine(this.documentManager.activeTextEditor, id); + return activeCodeWatcher.runSelectionOrLine(this.documentManager.activeTextEditor); } else { return Promise.resolve(); } @@ -189,10 +182,10 @@ export class DataScience implements IDataScience { private onSettingsChanged = () => { const settings = this.configuration.getSettings(); const enabled = settings.datascience.enabled; - let editorContext = new ContextKey(EditorContexts.DataScienceEnabled, this.commandBroker); + let editorContext = new ContextKey(EditorContexts.DataScienceEnabled, this.commandManager); editorContext.set(enabled).catch(); const ownsSelection = settings.datascience.sendSelectionToInteractiveWindow; - editorContext = new ContextKey(EditorContexts.OwnsSelection, this.commandBroker); + editorContext = new ContextKey(EditorContexts.OwnsSelection, this.commandManager); editorContext.set(ownsSelection && enabled).catch(); } @@ -219,26 +212,26 @@ export class DataScience implements IDataScience { } private registerCommands(): void { - let disposable = this.commandBroker.registerCommand(Commands.RunAllCells, this.runAllCells, this); + let disposable = this.commandManager.registerCommand(Commands.RunAllCells, this.runAllCells, this); this.disposableRegistry.push(disposable); - disposable = this.commandBroker.registerCommand(Commands.RunCell, this.runCell, this); + disposable = this.commandManager.registerCommand(Commands.RunCell, this.runCell, this); this.disposableRegistry.push(disposable); - disposable = this.commandBroker.registerCommand(Commands.RunCurrentCell, this.runCurrentCell, this); + disposable = this.commandManager.registerCommand(Commands.RunCurrentCell, this.runCurrentCell, this); this.disposableRegistry.push(disposable); - disposable = this.commandBroker.registerCommand(Commands.RunCurrentCellAdvance, this.runCurrentCellAndAdvance, this); + disposable = this.commandManager.registerCommand(Commands.RunCurrentCellAdvance, this.runCurrentCellAndAdvance, this); this.disposableRegistry.push(disposable); - disposable = this.commandBroker.registerCommand(Commands.ExecSelectionInInteractiveWindow, this.runSelectionOrLine, this); + disposable = this.commandManager.registerCommand(Commands.ExecSelectionInInteractiveWindow, this.runSelectionOrLine, this); this.disposableRegistry.push(disposable); - disposable = this.commandBroker.registerCommand(Commands.SelectJupyterURI, this.selectJupyterURI, this); + disposable = this.commandManager.registerCommand(Commands.SelectJupyterURI, this.selectJupyterURI, this); this.disposableRegistry.push(disposable); this.commandListeners.forEach((listener: IDataScienceCommandListener) => { - listener.register(this.commandBroker); + listener.register(this.commandManager); }); } private onChangedActiveTextEditor() { // Setup the editor context for the cells - const editorContext = new ContextKey(EditorContexts.HasCodeCells, this.commandBroker); + const editorContext = new ContextKey(EditorContexts.HasCodeCells, this.commandManager); const activeEditor = this.documentManager.activeTextEditor; if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 8996ac0781c5..70c765a7ccd3 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -1,243 +1,242 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import * as uuid from 'uuid/v4'; -import { CodeLens, Command, Position, Range, Selection, TextDocument, TextEditor, TextEditorRevealType } from 'vscode'; - -import { IApplicationShell, IDocumentManager } from '../../common/application/types'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDataScienceSettings, ILogger } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { captureTelemetry } from '../../telemetry'; -import { generateCellRanges } from '../cellFactory'; -import { Commands, Telemetry } from '../constants'; -import { JupyterInstallError } from '../jupyter/jupyterInstallError'; -import { ICodeWatcher, IHistoryProvider } from '../types'; - -@injectable() -export class CodeWatcher implements ICodeWatcher { - private document?: TextDocument; - private version: number = -1; - private fileName: string = ''; - private codeLenses: CodeLens[] = []; - private cachedSettings: IDataScienceSettings | undefined; - - constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(ILogger) private logger: ILogger, - @inject(IHistoryProvider) private historyProvider : IHistoryProvider, - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(IDocumentManager) private documentManager : IDocumentManager) {} - - public setDocument(document: TextDocument) { - this.document = document; - - // Cache these, we don't want to pull an old version if the document is updated - this.fileName = document.fileName; - this.version = document.version; - - // Get document cells here. Make a copy of our settings. - this.cachedSettings = JSON.parse(JSON.stringify(this.configService.getSettings().datascience)); - const cells = generateCellRanges(document, this.cachedSettings); - - this.codeLenses = []; - // Be careful here. These arguments will be serialized during liveshare sessions - // and so shouldn't reference local objects. - cells.forEach(cell => { - const cmd: Command = { - arguments: [document.fileName, cell.range.start.line, cell.range.start.character, cell.range.end.line, cell.range.end.character], - title: localize.DataScience.runCellLensCommandTitle(), - command: Commands.RunCell - }; - this.codeLenses.push(new CodeLens(cell.range, cmd)); - const runAllCmd: Command = { - arguments: [document.fileName], - title: localize.DataScience.runAllCellsLensCommandTitle(), - command: Commands.RunAllCells - }; - this.codeLenses.push(new CodeLens(cell.range, runAllCmd)); - }); - } - - public getFileName() { - return this.fileName; - } - - public getVersion() { - return this.version; - } - - public getCachedSettings() : IDataScienceSettings | undefined { - return this.cachedSettings; - } - - public getCodeLenses() { - return this.codeLenses; - } - - @captureTelemetry(Telemetry.RunAllCells) - public async runAllCells(id: string) { - const activeHistory = this.historyProvider.getOrCreateActive(); - - // Run all of our code lenses, they should always be ordered in the file so we can just - // run them one by one - for (const lens of this.codeLenses) { - // Make sure that we have the correct command (RunCell) lenses - if (lens.command && lens.command.command === Commands.RunCell && lens.command.arguments && lens.command.arguments.length >= 5) { - const range: Range = new Range(lens.command.arguments[1], lens.command.arguments[2], lens.command.arguments[3], lens.command.arguments[4]); - if (this.document && range) { - const code = this.document.getText(range); - await activeHistory.addCode(code, this.getFileName(), range.start.line, uuid()); - } - } - } - - // If there are no codelenses, just run all of the code as a single cell - if (this.codeLenses.length === 0) { - if (this.document) { - const code = this.document.getText(); - await activeHistory.addCode(code, this.getFileName(), 0, uuid()); - } - } - } - - @captureTelemetry(Telemetry.RunSelectionOrLine) - public async runSelectionOrLine(activeEditor : TextEditor | undefined, id: string) { - const activeHistory = this.historyProvider.getOrCreateActive(); - - if (this.document && activeEditor && - this.fileSystem.arePathsSame(activeEditor.document.fileName, this.document.fileName)) { - - // Get just the text of the selection or the current line if none - let code: string; - if (activeEditor.selection.start.line === activeEditor.selection.end.line && - activeEditor.selection.start.character === activeEditor.selection.end.character) { - const line = this.document.lineAt(activeEditor.selection.start.line); - code = line.text; - } else { - code = this.document.getText(new Range(activeEditor.selection.start, activeEditor.selection.end)); - } - - if (code && code.trim().length) { - await activeHistory.addCode(code, this.getFileName(), activeEditor.selection.start.line, uuid(), activeEditor); - } - } - } - - @captureTelemetry(Telemetry.RunCell) - public async runCell(range: Range, id: string) { - const activeHistory = this.historyProvider.getOrCreateActive(); - if (this.document) { - // Use that to get our code. - const code = this.document.getText(range); - - try { - await activeHistory.addCode(code, this.getFileName(), range.start.line, uuid(), this.documentManager.activeTextEditor); - } catch (err) { - this.handleError(err); - } - - } - } - - @captureTelemetry(Telemetry.RunCurrentCell) - public async runCurrentCell(id: string) { - if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return; - } - - for (const lens of this.codeLenses) { - // Check to see which RunCell lens range overlaps the current selection start - if (lens.range.contains(this.documentManager.activeTextEditor.selection.start) && lens.command && lens.command.command === Commands.RunCell) { - await this.runCell(lens.range, id); - break; - } - } - } - - @captureTelemetry(Telemetry.RunCurrentCellAndAdvance) - public async runCurrentCellAndAdvance(id: string) { - if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return; - } - - let currentRunCellLens: CodeLens | undefined; - let nextRunCellLens: CodeLens | undefined; - - for (const lens of this.codeLenses) { - // If we have already found the current code lens, then the next run cell code lens will give us the next cell - if (currentRunCellLens && lens.command && lens.command.command === Commands.RunCell) { - nextRunCellLens = lens; - break; - } - - // Check to see which RunCell lens range overlaps the current selection start - if (lens.range.contains(this.documentManager.activeTextEditor.selection.start) && lens.command && lens.command.command === Commands.RunCell) { - currentRunCellLens = lens; - } - } - - if (currentRunCellLens) { - // Either use the next cell that we found, or add a new one into the document - let nextRange: Range; - if (!nextRunCellLens) { - nextRange = this.createNewCell(currentRunCellLens.range); - } else { - nextRange = nextRunCellLens.range; - } - - if (nextRange) { - this.advanceToRange(nextRange); - } - - // Run the cell after moving the selection - await this.runCell(currentRunCellLens.range, id); - } - } - - // tslint:disable-next-line:no-any - private handleError = (err : any) => { - if (err instanceof JupyterInstallError) { - const jupyterError = err as JupyterInstallError; - - // This is a special error that shows a link to open for more help - this.applicationShell.showErrorMessage(jupyterError.message, jupyterError.actionTitle).then(v => { - // User clicked on the link, open it. - if (v === jupyterError.actionTitle) { - this.applicationShell.openUrl(jupyterError.action); - } - }); - } else if (err.message) { - this.applicationShell.showErrorMessage(err.message); - } else { - this.applicationShell.showErrorMessage(err.toString()); - } - this.logger.logError(err); - } - - // User has picked run and advance on the last cell of a document - // Create a new cell at the bottom and put their selection there, ready to type - private createNewCell(currentRange: Range): Range { - const editor = this.documentManager.activeTextEditor; - const newPosition = new Position(currentRange.end.line + 3, 0); // +3 to account for the added spaces and to position after the new mark - - if (editor) { - editor.edit((editBuilder) => { - editBuilder.insert(new Position(currentRange.end.line + 1, 0), '\n\n#%%\n'); - }); - } - - return new Range(newPosition, newPosition); - } - - // Advance the cursor to the selected range - private advanceToRange(targetRange: Range) { - const editor = this.documentManager.activeTextEditor; - const newSelection = new Selection(targetRange.start, targetRange.start); - if (editor) { - editor.selection = newSelection; - editor.revealRange(targetRange, TextEditorRevealType.Default); - } - } -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { CodeLens, Command, Position, Range, Selection, TextDocument, TextEditor, TextEditorRevealType } from 'vscode'; + +import { IApplicationShell, IDocumentManager } from '../../common/application/types'; +import { IFileSystem } from '../../common/platform/types'; +import { IConfigurationService, IDataScienceSettings, ILogger } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { captureTelemetry } from '../../telemetry'; +import { generateCellRanges } from '../cellFactory'; +import { Commands, Telemetry } from '../constants'; +import { JupyterInstallError } from '../jupyter/jupyterInstallError'; +import { ICodeWatcher, IHistoryProvider } from '../types'; + +@injectable() +export class CodeWatcher implements ICodeWatcher { + private document?: TextDocument; + private version: number = -1; + private fileName: string = ''; + private codeLenses: CodeLens[] = []; + private cachedSettings: IDataScienceSettings | undefined; + + constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell, + @inject(ILogger) private logger: ILogger, + @inject(IHistoryProvider) private historyProvider : IHistoryProvider, + @inject(IFileSystem) private fileSystem: IFileSystem, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IDocumentManager) private documentManager : IDocumentManager) {} + + public setDocument(document: TextDocument) { + this.document = document; + + // Cache these, we don't want to pull an old version if the document is updated + this.fileName = document.fileName; + this.version = document.version; + + // Get document cells here. Make a copy of our settings. + this.cachedSettings = JSON.parse(JSON.stringify(this.configService.getSettings().datascience)); + const cells = generateCellRanges(document, this.cachedSettings); + + this.codeLenses = []; + // Be careful here. These arguments will be serialized during liveshare sessions + // and so shouldn't reference local objects. + cells.forEach(cell => { + const cmd: Command = { + arguments: [document.fileName, cell.range.start.line, cell.range.start.character, cell.range.end.line, cell.range.end.character], + title: localize.DataScience.runCellLensCommandTitle(), + command: Commands.RunCell + }; + this.codeLenses.push(new CodeLens(cell.range, cmd)); + const runAllCmd: Command = { + arguments: [document.fileName], + title: localize.DataScience.runAllCellsLensCommandTitle(), + command: Commands.RunAllCells + }; + this.codeLenses.push(new CodeLens(cell.range, runAllCmd)); + }); + } + + public getFileName() { + return this.fileName; + } + + public getVersion() { + return this.version; + } + + public getCachedSettings() : IDataScienceSettings | undefined { + return this.cachedSettings; + } + + public getCodeLenses() { + return this.codeLenses; + } + + @captureTelemetry(Telemetry.RunAllCells) + public async runAllCells() { + const activeHistory = await this.historyProvider.getOrCreateActive(); + + // Run all of our code lenses, they should always be ordered in the file so we can just + // run them one by one + for (const lens of this.codeLenses) { + // Make sure that we have the correct command (RunCell) lenses + if (lens.command && lens.command.command === Commands.RunCell && lens.command.arguments && lens.command.arguments.length >= 5) { + const range: Range = new Range(lens.command.arguments[1], lens.command.arguments[2], lens.command.arguments[3], lens.command.arguments[4]); + if (this.document && range) { + const code = this.document.getText(range); + await activeHistory.addCode(code, this.getFileName(), range.start.line); + } + } + } + + // If there are no codelenses, just run all of the code as a single cell + if (this.codeLenses.length === 0) { + if (this.document) { + const code = this.document.getText(); + await activeHistory.addCode(code, this.getFileName(), 0); + } + } + } + + @captureTelemetry(Telemetry.RunSelectionOrLine) + public async runSelectionOrLine(activeEditor : TextEditor | undefined) { + const activeHistory = await this.historyProvider.getOrCreateActive(); + + if (this.document && activeEditor && + this.fileSystem.arePathsSame(activeEditor.document.fileName, this.document.fileName)) { + + // Get just the text of the selection or the current line if none + let code: string; + if (activeEditor.selection.start.line === activeEditor.selection.end.line && + activeEditor.selection.start.character === activeEditor.selection.end.character) { + const line = this.document.lineAt(activeEditor.selection.start.line); + code = line.text; + } else { + code = this.document.getText(new Range(activeEditor.selection.start, activeEditor.selection.end)); + } + + if (code && code.trim().length) { + await activeHistory.addCode(code, this.getFileName(), activeEditor.selection.start.line, activeEditor); + } + } + } + + @captureTelemetry(Telemetry.RunCell) + public async runCell(range: Range) { + const activeHistory = await this.historyProvider.getOrCreateActive(); + if (this.document) { + // Use that to get our code. + const code = this.document.getText(range); + + try { + await activeHistory.addCode(code, this.getFileName(), range.start.line, this.documentManager.activeTextEditor); + } catch (err) { + this.handleError(err); + } + + } + } + + @captureTelemetry(Telemetry.RunCurrentCell) + public async runCurrentCell() { + if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { + return; + } + + for (const lens of this.codeLenses) { + // Check to see which RunCell lens range overlaps the current selection start + if (lens.range.contains(this.documentManager.activeTextEditor.selection.start) && lens.command && lens.command.command === Commands.RunCell) { + await this.runCell(lens.range); + break; + } + } + } + + @captureTelemetry(Telemetry.RunCurrentCellAndAdvance) + public async runCurrentCellAndAdvance() { + if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { + return; + } + + let currentRunCellLens: CodeLens | undefined; + let nextRunCellLens: CodeLens | undefined; + + for (const lens of this.codeLenses) { + // If we have already found the current code lens, then the next run cell code lens will give us the next cell + if (currentRunCellLens && lens.command && lens.command.command === Commands.RunCell) { + nextRunCellLens = lens; + break; + } + + // Check to see which RunCell lens range overlaps the current selection start + if (lens.range.contains(this.documentManager.activeTextEditor.selection.start) && lens.command && lens.command.command === Commands.RunCell) { + currentRunCellLens = lens; + } + } + + if (currentRunCellLens) { + // Either use the next cell that we found, or add a new one into the document + let nextRange: Range; + if (!nextRunCellLens) { + nextRange = this.createNewCell(currentRunCellLens.range); + } else { + nextRange = nextRunCellLens.range; + } + + if (nextRange) { + this.advanceToRange(nextRange); + } + + // Run the cell after moving the selection + await this.runCell(currentRunCellLens.range); + } + } + + // tslint:disable-next-line:no-any + private handleError = (err : any) => { + if (err instanceof JupyterInstallError) { + const jupyterError = err as JupyterInstallError; + + // This is a special error that shows a link to open for more help + this.applicationShell.showErrorMessage(jupyterError.message, jupyterError.actionTitle).then(v => { + // User clicked on the link, open it. + if (v === jupyterError.actionTitle) { + this.applicationShell.openUrl(jupyterError.action); + } + }); + } else if (err.message) { + this.applicationShell.showErrorMessage(err.message); + } else { + this.applicationShell.showErrorMessage(err.toString()); + } + this.logger.logError(err); + } + + // User has picked run and advance on the last cell of a document + // Create a new cell at the bottom and put their selection there, ready to type + private createNewCell(currentRange: Range): Range { + const editor = this.documentManager.activeTextEditor; + const newPosition = new Position(currentRange.end.line + 3, 0); // +3 to account for the added spaces and to position after the new mark + + if (editor) { + editor.edit((editBuilder) => { + editBuilder.insert(new Position(currentRange.end.line + 1, 0), '\n\n#%%\n'); + }); + } + + return new Range(newPosition, newPosition); + } + + // Advance the cursor to the selected range + private advanceToRange(targetRange: Range) { + const editor = this.documentManager.activeTextEditor; + const newSelection = new Selection(targetRange.start, targetRange.start); + if (editor) { + editor.selection = newSelection; + editor.revealRange(targetRange, TextEditorRevealType.Default); + } + } +} diff --git a/src/client/datascience/history.ts b/src/client/datascience/history.ts index f45f6929d86f..a02ab5ef8d7d 100644 --- a/src/client/datascience/history.ts +++ b/src/client/datascience/history.ts @@ -6,8 +6,10 @@ import '../common/extensions'; import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as path from 'path'; +import * as uuid from 'uuid/v4'; import { Event, EventEmitter, Position, Range, Selection, TextEditor, Uri, ViewColumn } from 'vscode'; import { Disposable } from 'vscode-jsonrpc'; +import * as vsls from 'vsls/vscode'; import { IApplicationShell, @@ -27,8 +29,9 @@ import { createDeferred, Deferred } from '../common/utils/async'; import * as localize from '../common/utils/localize'; import { IInterpreterService } from '../interpreter/contracts'; import { captureTelemetry, sendTelemetryEvent } from '../telemetry'; -import { EditorContexts, HistoryMessages, HistoryNonLiveShareMessages, Identifiers, Telemetry } from './constants'; +import { EditorContexts, Identifiers, Telemetry } from './constants'; import { HistoryMessageListener } from './historyMessageListener'; +import { HistoryMessages, IAddedSysInfo, IGotoCode, IHistoryMapping, IRemoteAddCode, ISubmitNewCell } from './historyTypes'; import { JupyterInstallError } from './jupyter/jupyterInstallError'; import { CellState, @@ -38,10 +41,10 @@ import { IDataScienceExtraSettings, IHistory, IHistoryInfo, + IHistoryProvider, IJupyterExecution, INotebookExporter, INotebookServer, - INotebookServerManager, InterruptResult, IStatusProvider } from './types'; @@ -63,14 +66,15 @@ export class History implements IHistory { private unfinishedCells: ICell[] = []; private restartingKernel: boolean = false; private potentiallyUnfinishedStatus: Disposable[] = []; - private addedSysInfo: boolean = false; + private addSysInfoPromise: Deferred | undefined; private waitingForExportCells: boolean = false; private jupyterServer: INotebookServer | undefined; private changeHandler: IDisposable | undefined; private messageListener : HistoryMessageListener; + private id : string; constructor( - @inject(ILiveShareApi) liveShare : ILiveShareApi, + @inject(ILiveShareApi) private liveShare : ILiveShareApi, @inject(IApplicationShell) private applicationShell: IApplicationShell, @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IInterpreterService) private interpreterService: IInterpreterService, @@ -84,8 +88,12 @@ export class History implements IHistory { @inject(IConfigurationService) private configuration: IConfigurationService, @inject(ICommandManager) private commandManager: ICommandManager, @inject(INotebookExporter) private jupyterExporter: INotebookExporter, - @inject(INotebookServerManager) private jupyterServerManager: INotebookServerManager, - @inject(IWorkspaceService) private workspaceService: IWorkspaceService) { + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IHistoryProvider) private historyProvider: IHistoryProvider + ) { + + // Create our unique id. We use this to skip messages we send to other history windows + this.id = uuid(); // Sign up for configuration changes this.interpreterChangedDisposable = this.interpreterService.onDidChangeInterpreter(this.onInterpreterChanged); @@ -96,7 +104,7 @@ export class History implements IHistory { this.disposables.push(this.closedEvent); // Create a history message listener to listen to messages from our webpanel (or remote session) - this.messageListener = new HistoryMessageListener(liveShare, this.onMessage, this.dispose); + this.messageListener = new HistoryMessageListener(this.liveShare, this.onMessage, this.dispose); // Setup our init promise for the web panel. We use this to make sure we're in sync with our // react control. @@ -125,15 +133,19 @@ export class History implements IHistory { return this.closedEvent.event; } - public addCode(code: string, file: string, line: number, id: string, editor?: TextEditor) : Promise { + public addCode(code: string, file: string, line: number, editor?: TextEditor) : Promise { // Call the internal method. - return this.submitCode(code, file, line, id, editor); + return this.submitCode(code, file, line, undefined, editor); } // tslint:disable-next-line: no-any no-empty - public postMessage(type: string, payload?: any) { + public async postMessage(type: T, payload?: M[T]) : Promise { if (this.webPanel) { - this.webPanel.postMessage({ type: type, payload: payload }); + // Make sure the webpanel is up before we send it anything. + await this.webPanelInit.promise; + + // Then send it the message + this.webPanel.postMessage({ type: type.toString(), payload: payload }); } } @@ -141,7 +153,7 @@ export class History implements IHistory { public onMessage = (message: string, payload: any) => { switch (message) { case HistoryMessages.GotoCodeCell: - this.gotoCode(payload.file, payload.line); + this.dispatchMessage(message, payload, this.gotoCode); break; case HistoryMessages.RestartKernel: @@ -149,7 +161,7 @@ export class History implements IHistory { break; case HistoryMessages.ReturnAllCells: - this.handleReturnAllCells(payload); + this.dispatchMessage(message, payload, this.handleReturnAllCells); break; case HistoryMessages.Interrupt: @@ -157,19 +169,19 @@ export class History implements IHistory { break; case HistoryMessages.Export: - this.export(payload); + this.dispatchMessage(message, payload, this.export); break; - case HistoryNonLiveShareMessages.Started: - this.webPanelRendered(payload); + case HistoryMessages.Started: + this.webPanelRendered(); break; - case HistoryNonLiveShareMessages.SendInfo: - this.updateContexts(payload); + case HistoryMessages.SendInfo: + this.dispatchMessage(message, payload, this.updateContexts); break; case HistoryMessages.SubmitNewCell: - this.submitNewCell(payload); + this.dispatchMessage(message, payload, this.submitNewCell); break; case HistoryMessages.DeleteAllCells: @@ -196,6 +208,14 @@ export class History implements IHistory { this.logTelemetry(Telemetry.CollapseAll); break; + case HistoryMessages.AddedSysInfo: + this.dispatchMessage(message, payload, this.onAddedSysInfo); + break; + + case HistoryMessages.RemoteAddCode: + this.dispatchMessage(message, payload, this.onRemoteAddedCode); + break; + default: break; } @@ -210,7 +230,7 @@ export class History implements IHistory { if (this.closedEvent) { this.closedEvent.fire(this); } - this.updateContexts(); + this.updateContexts(undefined); } if (this.changeHandler) { this.changeHandler.dispose(); @@ -220,27 +240,27 @@ export class History implements IHistory { @captureTelemetry(Telemetry.Undo) public undoCells() { - this.postMessage(HistoryMessages.Undo); + this.postMessage(HistoryMessages.Undo).ignoreErrors(); } @captureTelemetry(Telemetry.Redo) public redoCells() { - this.postMessage(HistoryMessages.Redo); + this.postMessage(HistoryMessages.Redo).ignoreErrors(); } @captureTelemetry(Telemetry.DeleteAllCells) public removeAllCells() { - this.postMessage(HistoryMessages.DeleteAllCells); + this.postMessage(HistoryMessages.DeleteAllCells).ignoreErrors(); } @captureTelemetry(Telemetry.ExpandAll) public expandAllCells() { - this.postMessage(HistoryMessages.ExpandAll); + this.postMessage(HistoryMessages.ExpandAll).ignoreErrors(); } @captureTelemetry(Telemetry.CollapseAll) public collapseAllCells() { - this.postMessage(HistoryMessages.CollapseAll); + this.postMessage(HistoryMessages.CollapseAll).ignoreErrors(); } public exportCells() { @@ -248,7 +268,7 @@ export class History implements IHistory { this.waitingForExportCells = true; // Telemetry will fire when the export function is called. - this.postMessage(HistoryMessages.GetAllCells); + this.postMessage(HistoryMessages.GetAllCells).ignoreErrors(); } @captureTelemetry(Telemetry.RestartKernel) @@ -307,15 +327,44 @@ export class History implements IHistory { } } + // tslint:disable-next-line:no-any + private dispatchMessage(message: T, payload: any, handler: (args : M[T]) => void) { + const args = payload as M[T]; + handler.bind(this)(args); + } + + // tslint:disable-next-line:no-any + private onAddedSysInfo(sysInfo : IAddedSysInfo) { + // See if this is from us or not. + if (sysInfo.id !== this.id) { + + // Not from us, must come from a different history window. Add to our + // own to keep in sync + if (sysInfo.sysInfoCell) { + this.onAddCodeEvent([sysInfo.sysInfoCell]); + } + } + } + + // tslint:disable-next-line:no-any + private onRemoteAddedCode(args: IRemoteAddCode) { + // Make sure this is valid + if (args && args.id && args.file && args.originator !== this.id) { + // Indicate this in our telemetry. + sendTelemetryEvent(Telemetry.RemoteAddCode); + + // Submit this item as new code. + this.submitCode(args.code, args.file, args.line, args.id).ignoreErrors(); + } + } + private async restartKernelInternal(): Promise { this.restartingKernel = true; // First we need to finish all outstanding cells. this.unfinishedCells.forEach(c => { c.state = CellState.error; - if (this.webPanel) { - this.webPanel.postMessage({ type: HistoryMessages.FinishCell, payload: c }); - } + this.postMessage(HistoryMessages.FinishCell, c).ignoreErrors(); }); this.unfinishedCells = []; this.potentiallyUnfinishedStatus.forEach(s => s.dispose()); @@ -336,22 +385,22 @@ export class History implements IHistory { } // tslint:disable-next-line:no-any - private handleReturnAllCells = (payload: any) => { + private handleReturnAllCells(cells: ICell[]) { // See what we're waiting for. if (this.waitingForExportCells) { - this.export(payload); + this.export(cells); } } // tslint:disable-next-line:no-any - private webPanelRendered(payload? : any) { + private webPanelRendered() { if (!this.webPanelInit.resolved) { this.webPanelInit.resolve(); } } // tslint:disable-next-line:no-any - private updateContexts = (payload?: any) => { + private updateContexts(info: IHistoryInfo | undefined) { // This should be called by the python interactive window every // time state changes. We use this opportunity to update our // extension contexts @@ -359,15 +408,9 @@ export class History implements IHistory { interactiveContext.set(!this.disposed).catch(); const interactiveCellsContext = new ContextKey(EditorContexts.HaveInteractiveCells, this.commandManager); const redoableContext = new ContextKey(EditorContexts.HaveRedoableCells, this.commandManager); - if (payload && payload.info) { - const info = payload.info as IHistoryInfo; - if (info) { - interactiveCellsContext.set(info.cellCount > 0).catch(); - redoableContext.set(info.redoCount > 0).catch(); - } else { - interactiveCellsContext.set(false).catch(); - redoableContext.set(false).catch(); - } + if (info) { + interactiveCellsContext.set(info.cellCount > 0).catch(); + redoableContext.set(info.redoCount > 0).catch(); } else { interactiveCellsContext.set(false).catch(); redoableContext.set(false).catch(); @@ -376,14 +419,20 @@ export class History implements IHistory { @captureTelemetry(Telemetry.SubmitCellThroughInput, undefined, false) // tslint:disable-next-line:no-any - private submitNewCell(payload?: any) { + private submitNewCell(info: ISubmitNewCell) { // If there's any payload, it has the code and the id - if (payload && payload.code && payload.id) { - this.submitCode(payload.code, Identifiers.EmptyFileName, 0, payload.id, undefined).ignoreErrors(); + if (info && info.code && info.id) { + this.submitCode(info.code, Identifiers.EmptyFileName, 0, info.id, undefined).ignoreErrors(); } } - private async submitCode(code: string, file: string, line: number, id: string, editor?: TextEditor) : Promise { + private async submitCode(code: string, file: string, line: number, id?: string, editor?: TextEditor) : Promise { + // Transmit this submission to all other listeners (in a live share session) + if (!id) { + id = uuid(); + this.shareMessage(HistoryMessages.RemoteAddCode, {code, file, line, id, originator: this.id}); + } + // Start a status item const status = this.setStatus(localize.DataScience.executingCode()); @@ -463,12 +512,6 @@ export class History implements IHistory { sendTelemetryEvent(event); } - private sendCell(cell: ICell, message: string) { - if (this.webPanel) { - this.webPanel.postMessage({ type: message, payload: cell }); - } - } - private onAddCodeEvent = (cells: ICell[], editor?: TextEditor) => { // Send each cell to the other side cells.forEach((cell: ICell) => { @@ -476,7 +519,7 @@ export class History implements IHistory { switch (cell.state) { case CellState.init: // Tell the react controls we have a new cell - this.sendCell(cell, HistoryMessages.StartCell); + this.postMessage(HistoryMessages.StartCell, cell).ignoreErrors(); // Keep track of this unfinished cell so if we restart we can finish right away. this.unfinishedCells.push(cell); @@ -484,13 +527,13 @@ export class History implements IHistory { case CellState.executing: // Tell the react controls we have an update - this.sendCell(cell, HistoryMessages.UpdateCell); + this.postMessage(HistoryMessages.UpdateCell, cell).ignoreErrors(); break; case CellState.error: case CellState.finished: // Tell the react controls we're done - this.sendCell(cell, HistoryMessages.FinishCell); + this.postMessage(HistoryMessages.FinishCell, cell).ignoreErrors(); // Remove from the list of unfinished cells this.unfinishedCells = this.unfinishedCells.filter(c => c.id !== cell.id); @@ -517,10 +560,7 @@ export class History implements IHistory { private onSettingsChanged = () => { // Stringify our settings to send over to the panel const dsSettings = JSON.stringify(this.generateDataScienceExtraSettings()); - - if (this.webPanel) { - this.webPanel.postMessage({ type: HistoryMessages.UpdateSettings, payload: dsSettings }); - } + this.postMessage(HistoryMessages.UpdateSettings, dsSettings).ignoreErrors(); } private onInterpreterChanged = async () => { @@ -535,8 +575,8 @@ export class History implements IHistory { } @captureTelemetry(Telemetry.GotoSourceCode, undefined, false) - private gotoCode(file: string, line: number) { - this.gotoCodeInternal(file, line).catch(err => { + private gotoCode(args: IGotoCode) { + this.gotoCodeInternal(args.file, args.line).catch(err => { this.applicationShell.showErrorMessage(err); }); } @@ -563,27 +603,32 @@ export class History implements IHistory { @captureTelemetry(Telemetry.ExportNotebook, undefined, false) // tslint:disable-next-line: no-any no-empty - private export(payload: any) { - if (payload.contents) { - // Should be an array of cells - const cells = payload.contents as ICell[]; - if (cells && this.applicationShell) { - - const filtersKey = localize.DataScience.exportDialogFilter(); - const filtersObject: Record = {}; - filtersObject[filtersKey] = ['ipynb']; - - // Bring up the open file dialog box - this.applicationShell.showSaveDialog( - { - saveLabel: localize.DataScience.exportDialogTitle(), - filters: filtersObject - }).then(async (uri: Uri | undefined) => { - if (uri) { - await this.exportToFile(cells, uri.fsPath); - } - }); - } + private export(cells: ICell[]) { + // Should be an array of cells + if (cells && this.applicationShell) { + + const filtersKey = localize.DataScience.exportDialogFilter(); + const filtersObject: Record = {}; + filtersObject[filtersKey] = ['ipynb']; + + // Bring up the open file dialog box + this.applicationShell.showSaveDialog( + { + saveLabel: localize.DataScience.exportDialogTitle(), + filters: filtersObject + }).then(async (uri: Uri | undefined) => { + if (uri) { + await this.exportToFile(cells, uri.fsPath); + } + }); + } + } + + private showInformationMessage(message: string, question?: string) : Thenable { + if (question) { + return this.applicationShell.showInformationMessage(message, question); + } else { + return this.applicationShell.showInformationMessage(message); } } @@ -601,7 +646,8 @@ export class History implements IHistory { try { // tslint:disable-next-line: no-any await this.fileSystem.writeFile(file, JSON.stringify(notebook), { encoding: 'utf8', flag: 'w' }); - this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(file), localize.DataScience.exportOpenQuestion()).then((str: string | undefined) => { + const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined; + this.showInformationMessage(localize.DataScience.exportDialogComplete().format(file), openQuestion).then((str: string | undefined) => { if (str && this.jupyterServer) { // If the user wants to, open the notebook they just generated. this.jupyterExecution.spawnNotebook(file).ignoreErrors(); @@ -615,7 +661,11 @@ export class History implements IHistory { } private async loadJupyterServer(restart?: boolean): Promise { - this.jupyterServer = await this.jupyterServerManager.getOrCreateServer(); + // Extract our options + const options = await this.historyProvider.getNotebookOptions(); + + // Now try to create a notebook server + this.jupyterServer = await this.jupyterExecution.connectToNotebookServer(options); } private generateSysInfoCell = async (reason: SysInfoReason): Promise => { @@ -675,15 +725,32 @@ export class History implements IHistory { return `${localize.DataScience.sysInfoURILabel()}${urlString}`; } + private shareMessage(type: T, payload?: M[T]) { + this.messageListener.onMessage(type.toString(), payload); + } + private addSysInfo = async (reason: SysInfoReason): Promise => { - if (!this.addedSysInfo || reason === SysInfoReason.Interrupt || reason === SysInfoReason.Restart) { - this.addedSysInfo = true; + if (!this.addSysInfoPromise || reason === SysInfoReason.Interrupt || reason === SysInfoReason.Restart) { + this.logger.logInformation(`Adding sys info for ${reason}`); + const deferred = createDeferred(); + this.addSysInfoPromise = deferred; // Generate a new sys info cell and send it to the web panel. const sysInfo = await this.generateSysInfoCell(reason); if (sysInfo) { this.onAddCodeEvent([sysInfo]); } + + // For interrupt or restart, tell the other sides of a live share session + if ((reason === SysInfoReason.Interrupt || reason === SysInfoReason.Restart) && sysInfo) { + this.shareMessage(HistoryMessages.AddedSysInfo, { sysInfoCell: sysInfo, id: this.id }); + } + + this.logger.logInformation(`Sys info for ${reason} complete`); + deferred.resolve(true); + } else if (this.addSysInfoPromise) { + this.logger.logInformation(`Wait for sys info for ${reason}`); + await this.addSysInfoPromise.promise; } } @@ -699,31 +766,32 @@ export class History implements IHistory { } private loadWebPanel = async (): Promise => { + this.logger.logInformation(`Loading web panel. Panel is ${this.webPanel ? 'set' : 'notset'}`); + // Create our web panel (it's the UI that shows up for the history) if (this.webPanel === undefined) { // Figure out the name of our main bundle. Should be in our output directory const mainScriptPath = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'history-react', 'index_bundle.js'); + this.logger.logInformation('Generating CSS...'); // Generate a css to put into the webpanel for viewing code const css = await this.cssGenerator.generateThemeCss(); // Get our settings to pass along to the react control const settings = this.generateDataScienceExtraSettings(); + this.logger.logInformation('Loading web view...'); // Use this script to create our web view panel. It should contain all of the necessary // script to communicate with this class. this.webPanel = this.provider.create(this.messageListener, localize.DataScience.historyTitle(), mainScriptPath, css, settings); - // Wait for our web panel initialization message to appear. VS code doesn't give us a way - // to wait for the html to load. If we start interacting with the webpanel before it's ready, we - // miss out on handling messages. - await this.webPanelInit.promise; + this.logger.logInformation('Web view created.'); } } private load = async (): Promise => { // Status depends upon if we're about to connect to existing server or not. - const status = (await this.jupyterServerManager.getServer()) ? + const status = (await this.jupyterExecution.getServer(await this.historyProvider.getNotebookOptions())) ? this.setStatus(localize.DataScience.connectingToJupyter()) : this.setStatus(localize.DataScience.startingJupyter()); // Check to see if we support ipykernel or not @@ -737,13 +805,17 @@ export class History implements IHistory { throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); } else { // See if the usable interpreter is not our active one. If so, show a warning - const active = await this.interpreterService.getActiveInterpreter(); - const activeDisplayName = active ? active.displayName : undefined; - const activePath = active ? active.path : undefined; - const usableDisplayName = usableInterpreter ? usableInterpreter.displayName : undefined; - const usablePath = usableInterpreter ? usableInterpreter.path : undefined; - if (activePath && usablePath && !this.fileSystem.arePathsSame(activePath, usablePath) && activeDisplayName && usableDisplayName) { - this.applicationShell.showWarningMessage(localize.DataScience.jupyterKernelNotSupportedOnActive().format(activeDisplayName, usableDisplayName)); + // Only do this if not the guest in a liveshare session + const api = await this.liveShare.getApi(); + if (!api || (api.session && api.session.role !== vsls.Role.Guest)) { + const active = await this.interpreterService.getActiveInterpreter(); + const activeDisplayName = active ? active.displayName : undefined; + const activePath = active ? active.path : undefined; + const usableDisplayName = usableInterpreter ? usableInterpreter.displayName : undefined; + const usablePath = usableInterpreter ? usableInterpreter.path : undefined; + if (activePath && usablePath && !this.fileSystem.arePathsSame(activePath, usablePath) && activeDisplayName && usableDisplayName) { + this.applicationShell.showWarningMessage(localize.DataScience.jupyterKernelNotSupportedOnActive().format(activeDisplayName, usableDisplayName)); + } } } diff --git a/src/client/datascience/historyMessageListener.ts b/src/client/datascience/historyMessageListener.ts index faa97d431ce2..14cd19fa5c67 100644 --- a/src/client/datascience/historyMessageListener.ts +++ b/src/client/datascience/historyMessageListener.ts @@ -3,8 +3,12 @@ 'use strict'; import '../common/extensions'; +import * as vscode from 'vscode'; +import * as vsls from 'vsls/vscode'; + import { ILiveShareApi, IWebPanelMessageListener } from '../common/application/types'; -import { HistoryMessages, LiveShare } from './constants'; +import { Identifiers, LiveShare } from './constants'; +import { HistoryMessages, HistoryRemoteMessages } from './historyTypes'; import { PostOffice } from './liveshare/postOffice'; // tslint:disable:no-any @@ -17,7 +21,7 @@ export class HistoryMessageListener implements IWebPanelMessageListener { private historyMessages : string[] = []; constructor(liveShare: ILiveShareApi, callback: (message: string, payload: any) => void, disposed: () => void) { - this.postOffice = new PostOffice(LiveShare.WebPanelMessageService, liveShare); + this.postOffice = new PostOffice(LiveShare.WebPanelMessageService, liveShare, (api, command, role, args) => this.translateHostArgs(api, role, args)); // Save our dispose callback so we remove our history window this.disposedCallback = disposed; @@ -40,8 +44,8 @@ export class HistoryMessageListener implements IWebPanelMessageListener { } public onMessage(message: string, payload: any) { - // We received a message from the local webview. Broadcast it to everybody if it's a history message - if (this.historyMessages.indexOf(message) >= 0) { + // We received a message from the local webview. Broadcast it to everybody if it's a remote message + if (HistoryRemoteMessages.indexOf(message) >= 0) { this.postOffice.postCommand(message, payload).ignoreErrors(); } else { // Send to just our local callback. @@ -52,4 +56,34 @@ export class HistoryMessageListener implements IWebPanelMessageListener { private getHistoryMessages() : string [] { return Object.keys(HistoryMessages).map(k => (HistoryMessages as any)[k].toString()); } + + private translateHostArgs(api: vsls.LiveShare | null, role: vsls.Role, args: any[]) { + // Figure out the true type of the args + if (api && args && args.length > 0) { + const trueArg = args[0]; + + // See if the trueArg has a 'file' name or not + if (trueArg) { + const keys = Object.keys(trueArg); + keys.forEach(k => { + if (k.includes('file')) { + if (typeof trueArg[k] === 'string') { + // Pull out the string. We need to convert it to a file or vsls uri based on our role + const file = trueArg[k].toString(); + + // Skip the empty file + if (file !== Identifiers.EmptyFileName) { + const uri = role === vsls.Role.Host ? vscode.Uri.file(file) : vscode.Uri.parse(`vsls:${file}`); + + // Translate this into the other side. + trueArg[k] = role === vsls.Role.Host ? + api.convertLocalUriToShared(uri).fsPath : + api.convertSharedUriToLocal(uri).fsPath; + } + } + } + }); + } + } + } } diff --git a/src/client/datascience/historyProvider.ts b/src/client/datascience/historyProvider.ts index 9f19b8e0b45c..003de76e5b6f 100644 --- a/src/client/datascience/historyProvider.ts +++ b/src/client/datascience/historyProvider.ts @@ -2,33 +2,97 @@ // Licensed under the MIT License. 'use strict'; import { inject, injectable } from 'inversify'; +import * as uuid from 'uuid/v4'; -import { IDisposableRegistry } from '../common/types'; +import { ILiveShareApi, IWorkspaceService } from '../common/application/types'; +import { IAsyncDisposable, IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry } from '../common/types'; +import { createDeferred, Deferred } from '../common/utils/async'; import { IServiceContainer } from '../ioc/types'; -import { IHistory, IHistoryProvider } from './types'; +import { Identifiers, LiveShare, LiveShareCommands, Settings } from './constants'; +import { PostOffice } from './liveshare/postOffice'; +import { IHistory, IHistoryProvider, INotebookServerOptions, IThemeFinder } from './types'; @injectable() -export class HistoryProvider implements IHistoryProvider { +export class HistoryProvider implements IHistoryProvider, IAsyncDisposable { private activeHistory : IHistory | undefined; - + private postOffice : PostOffice; + private id: string; + private pendingSyncs : { [key: string] : { waitable: Deferred; count: number }} = {}; constructor( + @inject(ILiveShareApi) liveShare: ILiveShareApi, @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IDisposableRegistry) private disposables: IDisposableRegistry) { + @inject(IAsyncDisposableRegistry) asyncRegistry : IAsyncDisposableRegistry, + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IThemeFinder) private themeFinder: IThemeFinder + ) { + asyncRegistry.push(this); + + // Create a post office so we can make sure history windows are created at the same time + // on both sides. + this.postOffice = new PostOffice(LiveShare.HistoryProviderService, liveShare); + + // Listen for peer changes + this.postOffice.peerCountChanged((n) => this.onPeerCountChanged(n)); + + // Listen for messages so we force a create on both sides. + this.postOffice.registerCallback(LiveShareCommands.historyCreate, this.onRemoteCreate, this).ignoreErrors(); + this.postOffice.registerCallback(LiveShareCommands.historyCreateSync, this.onRemoteSync, this).ignoreErrors(); + + // Make a unique id so we can tell who sends a message + this.id = uuid(); } public getActive() : IHistory | undefined { return this.activeHistory; } - public getOrCreateActive() : IHistory { + public async getOrCreateActive() : Promise { if (!this.activeHistory) { this.activeHistory = this.create(); } + // Make sure all other providers have an active history. + await this.synchronizeCreate(); + + // Now that all of our peers have sync'd, return the history to use. return this.activeHistory; } + public async getNotebookOptions() : Promise { + // Find the settings that we are going to launch our server with + const settings = this.configService.getSettings(); + let serverURI: string | undefined = settings.datascience.jupyterServerURI; + const useDefaultConfig: boolean | undefined = settings.datascience.useDefaultConfigForJupyter; + // Check for dark theme, if so set matplot lib to use dark_background settings + let darkTheme: boolean | undefined = false; + const workbench = this.workspaceService.getConfiguration('workbench'); + if (workbench) { + const theme = workbench.get('colorTheme'); + if (theme) { + darkTheme = await this.themeFinder.isThemeDark(theme); + } + } + + // For the local case pass in our URI as undefined, that way connect doesn't have to check the setting + if (serverURI === Settings.JupyterServerLocalLaunch) { + serverURI = undefined; + } + + return { + uri: serverURI, + usingDarkTheme: darkTheme, + useDefaultConfig, + purpose: Identifiers.HistoryPurpose + }; + } + + public dispose() : Promise { + return this.postOffice.dispose(); + } + private create = () => { const result = this.serviceContainer.get(IHistory); const handler = result.closed(this.onHistoryClosed); @@ -37,10 +101,65 @@ export class HistoryProvider implements IHistoryProvider { return result; } + private onPeerCountChanged(newCount: number) { + // If we're losing peers, resolve all syncs + if (newCount < this.postOffice.peerCount) { + Object.keys(this.pendingSyncs).forEach(k => this.pendingSyncs[k].waitable.resolve()); + this.pendingSyncs = {}; + } + } + + // tslint:disable-next-line:no-any + private onRemoteCreate(...args: any[]) { + // Should be a single arg, the originator of the create + if (args.length > 0 && args[0].toString() !== this.id) { + // The other side is creating a history window. Create on this side. We don't need to show + // it as the running of new code should do that. + if (!this.activeHistory) { + this.activeHistory = this.create(); + } + + // Tell the requestor that we got its message (it should be waiting for all peers to sync) + this.postOffice.postCommand(LiveShareCommands.historyCreateSync, ...args).ignoreErrors(); + } + } + + // tslint:disable-next-line:no-any + private onRemoteSync(...args: any[]) { + // Should be a single arg, the originator of the create + if (args.length > 1 && args[0].toString() === this.id) { + // Update our pending wait count on the matching pending sync + const key = args[1].toString(); + if (this.pendingSyncs.hasOwnProperty(key)) { + this.pendingSyncs[key].count -= 1; + if (this.pendingSyncs[key].count <= 0) { + this.pendingSyncs[key].waitable.resolve(); + } + } + } + } + private onHistoryClosed = (history: IHistory) => { if (this.activeHistory === history) { this.activeHistory = undefined; } } + private synchronizeCreate() : Promise { + // Create a new pending wait if necessary + if (this.postOffice.peerCount > 0) { + const key = uuid(); + const waitable = createDeferred(); + this.pendingSyncs[key] = { count: this.postOffice.peerCount, waitable }; + + // Make sure all providers have an active history + this.postOffice.postCommand(LiveShareCommands.historyCreate, this.id, key).ignoreErrors(); + + // Wait for the waitable to be signaled or the peer count on the post office to change + return waitable.promise; + } + + return Promise.resolve(); + } + } diff --git a/src/client/datascience/historyTypes.ts b/src/client/datascience/historyTypes.ts new file mode 100644 index 000000000000..ce1015d50a5a --- /dev/null +++ b/src/client/datascience/historyTypes.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { ICell, IHistoryInfo } from './types'; + +export namespace HistoryMessages { + export const StartCell = 'start_cell'; + export const FinishCell = 'finish_cell'; + export const UpdateCell = 'update_cell'; + export const GotoCodeCell = 'gotocell_code'; + export const RestartKernel = 'restart_kernel'; + export const Export = 'export_to_ipynb'; + export const GetAllCells = 'get_all_cells'; + export const ReturnAllCells = 'return_all_cells'; + export const DeleteCell = 'delete_cell'; + export const DeleteAllCells = 'delete_all_cells'; + export const Undo = 'undo'; + export const Redo = 'redo'; + export const ExpandAll = 'expand_all'; + export const CollapseAll = 'collapse_all'; + export const StartProgress = 'start_progress'; + export const StopProgress = 'stop_progress'; + export const Interrupt = 'interrupt'; + export const SubmitNewCell = 'submit_new_cell'; + export const UpdateSettings = 'update_settings'; + export const SendInfo = 'send_info'; + export const Started = 'started'; + export const AddedSysInfo = 'added_sys_info'; + export const RemoteAddCode = 'remote_add_code'; +} + +// These are the messages that will mirror'd to guest/hosts in +// a live share session +export const HistoryRemoteMessages : string[] = [ + HistoryMessages.SubmitNewCell, + HistoryMessages.AddedSysInfo, + HistoryMessages.RemoteAddCode +]; + +export interface IGotoCode { + file: string; + line: number; +} + +export interface IAddedSysInfo { + id: string; + sysInfoCell: ICell; +} + +export interface IExecuteInfo { + code: string; + id: string; + file: string; + line: number; +} + +export interface IRemoteAddCode extends IExecuteInfo { + originator: string; +} + +export interface ISubmitNewCell { + code: string; + id: string; +} + +// Map all messages to specific payloads +export class IHistoryMapping { + public [HistoryMessages.StartCell]: ICell; + public [HistoryMessages.FinishCell]: ICell; + public [HistoryMessages.UpdateCell]: ICell; + public [HistoryMessages.GotoCodeCell]: IGotoCode; + public [HistoryMessages.RestartKernel]: never | undefined; + public [HistoryMessages.Export]: ICell[]; + public [HistoryMessages.GetAllCells]: ICell; + public [HistoryMessages.ReturnAllCells]: ICell[]; + public [HistoryMessages.DeleteCell]: never | undefined; + public [HistoryMessages.DeleteAllCells]: never | undefined; + public [HistoryMessages.Undo]: never | undefined; + public [HistoryMessages.Redo]: never | undefined; + public [HistoryMessages.ExpandAll]: never | undefined; + public [HistoryMessages.CollapseAll]: never | undefined; + public [HistoryMessages.StartProgress]: never | undefined; + public [HistoryMessages.StopProgress]: never | undefined; + public [HistoryMessages.Interrupt]: never | undefined; + public [HistoryMessages.UpdateSettings]: string; + public [HistoryMessages.SubmitNewCell]: ISubmitNewCell; + public [HistoryMessages.SendInfo]: IHistoryInfo; + public [HistoryMessages.Started]: never | undefined; + public [HistoryMessages.AddedSysInfo]: IAddedSysInfo; + public [HistoryMessages.RemoteAddCode]: IRemoteAddCode; +} diff --git a/src/client/datascience/historycommandlistener.ts b/src/client/datascience/historycommandlistener.ts index f8bd51f82a19..4378b0f44e2e 100644 --- a/src/client/datascience/historycommandlistener.ts +++ b/src/client/datascience/historycommandlistener.ts @@ -8,7 +8,7 @@ import * as uuid from 'uuid/v4'; import { Position, Range, TextDocument, Uri, ViewColumn } from 'vscode'; import { CancellationToken, CancellationTokenSource } from 'vscode-jsonrpc'; -import { IApplicationShell, IDocumentManager } from '../common/application/types'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; import { CancellationError } from '../common/cancellation'; import { PYTHON_LANGUAGE } from '../common/constants'; import { IFileSystem } from '../common/platform/types'; @@ -19,7 +19,6 @@ import { CommandSource } from '../unittests/common/constants'; import { generateCellRanges, generateCellsFromDocument } from './cellFactory'; import { Commands, Telemetry } from './constants'; import { - ICommandBroker, IDataScienceCommandListener, IHistoryProvider, IJupyterExecution, @@ -49,7 +48,7 @@ export class HistoryCommandListener implements IDataScienceCommandListener { this.disposableRegistry.push(disposable); } - public register(commandManager: ICommandBroker): void { + public register(commandManager: ICommandManager): void { let disposable = commandManager.registerCommand(Commands.ShowHistoryPane, () => this.showHistoryPane()); this.disposableRegistry.push(disposable); disposable = commandManager.registerCommand(Commands.ImportNotebook, async (file: Uri, cmdSource: CommandSource = CommandSource.commandPalette) => { @@ -116,6 +115,14 @@ export class HistoryCommandListener implements IDataScienceCommandListener { } } + private showInformationMessage(message: string, question?: string) : Thenable { + if (question) { + return this.applicationShell.showInformationMessage(message, question); + } else { + return this.applicationShell.showInformationMessage(message); + } + } + @captureTelemetry(Telemetry.ExportPythonFile, undefined, false) private async exportFile(file: string): Promise { if (file && file.length > 0) { @@ -148,9 +155,9 @@ export class HistoryCommandListener implements IDataScienceCommandListener { }, localize.DataScience.exportingFormat(), file); // When all done, show a notice that it completed. - const openQuestion = localize.DataScience.exportOpenQuestion(); + const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined; if (uri && uri.fsPath) { - this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(uri.fsPath), openQuestion).then((str: string | undefined) => { + this.showInformationMessage(localize.DataScience.exportDialogComplete().format(uri.fsPath), openQuestion).then((str: string | undefined) => { if (str === openQuestion) { // If the user wants to, open the notebook they just generated. this.jupyterExecution.spawnNotebook(uri.fsPath).ignoreErrors(); @@ -185,7 +192,7 @@ export class HistoryCommandListener implements IDataScienceCommandListener { return this.exportCellsWithOutput(ranges, activeEditor.document, output, cancelSource.token); } catch (err) { if (!(err instanceof CancellationError)) { - this.applicationShell.showInformationMessage(localize.DataScience.exportDialogFailed().format(err)); + this.showInformationMessage(localize.DataScience.exportDialogFailed().format(err)); } } return Promise.resolve(); @@ -194,8 +201,8 @@ export class HistoryCommandListener implements IDataScienceCommandListener { }, true); // When all done, show a notice that it completed. - const openQuestion = localize.DataScience.exportOpenQuestion(); - this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(output), openQuestion).then((str: string | undefined) => { + const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined; + this.showInformationMessage(localize.DataScience.exportDialogComplete().format(output), openQuestion).then((str: string | undefined) => { if (str === openQuestion && output) { // If the user wants to, open the notebook they just generated. this.jupyterExecution.spawnNotebook(output).ignoreErrors(); @@ -216,8 +223,9 @@ export class HistoryCommandListener implements IDataScienceCommandListener { const settings = this.configuration.getSettings(); const useDefaultConfig : boolean | undefined = settings.datascience.useDefaultConfigForJupyter; - // Try starting a server. - server = await this.jupyterExecution.connectToNotebookServer(undefined, false, useDefaultConfig, cancelToken); + // Try starting a server. Purpose should be unique so we + // create a brand new one. + server = await this.jupyterExecution.connectToNotebookServer({ useDefaultConfig, purpose: uuid()}, cancelToken); // If that works, then execute all of the cells. const cells = Array.prototype.concat(... await Promise.all(ranges.map(r => { @@ -347,8 +355,8 @@ export class HistoryCommandListener implements IDataScienceCommandListener { } @captureTelemetry(Telemetry.ShowHistoryPane, undefined, false) - private showHistoryPane() : Promise{ - const active = this.historyProvider.getOrCreateActive(); + private async showHistoryPane() : Promise{ + const active = await this.historyProvider.getOrCreateActive(); return active.show(); } diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts index 31292ba03337..c9edf82a31cf 100644 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -29,7 +29,8 @@ import { IJupyterKernelSpec, IJupyterSessionManager, INotebookServer, - INotebookServerLaunchInfo + INotebookServerLaunchInfo, + INotebookServerOptions } from '../types'; import { JupyterConnection, JupyterServerInfo } from './jupyterConnection'; import { JupyterKernelSpec } from './jupyterKernelSpec'; @@ -104,15 +105,20 @@ export class JupyterExecutionBase implements IJupyterExecution { return Cancellation.race(() => this.isCommandSupported(JupyterCommands.KernelSpecCommand), cancelToken); } - public connectToNotebookServer(uri: string | undefined, usingDarkTheme: boolean, useDefaultConfig: boolean, cancelToken?: CancellationToken, workingDir?: string): Promise { + public isSpawnSupported(cancelToken?: CancellationToken): Promise { + // Supported if we can run a notebook + return this.isNotebookSupported(cancelToken); + } + + public connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken): Promise { // Return nothing if we cancel return Cancellation.race(async () => { let connection: IConnection; let kernelSpec: IJupyterKernelSpec | undefined; // If our uri is undefined or if it's set to local launch we need to launch a server locally - if (!uri) { - const launchResults = await this.startNotebookServer(useDefaultConfig, cancelToken); + if (!options || !options.uri) { + const launchResults = await this.startNotebookServer(options && options.useDefaultConfig ? true : false, cancelToken); if (launchResults) { connection = launchResults.connection; kernelSpec = launchResults.kernelSpec; @@ -125,7 +131,7 @@ export class JupyterExecutionBase implements IJupyterExecution { } } else { // If we have a URI spec up a connection info for it - connection = this.createRemoteConnectionInfo(uri); + connection = this.createRemoteConnectionInfo(options.uri); kernelSpec = undefined; } @@ -148,12 +154,13 @@ export class JupyterExecutionBase implements IJupyterExecution { connectionInfo: connection, currentInterpreter: info, kernelSpec: kernelSpec, - usingDarkTheme: usingDarkTheme, - workingDir: workingDir, - uri: uri + usingDarkTheme: options && options.usingDarkTheme ? options.usingDarkTheme : false, + workingDir: options ? options.workingDir : undefined, + uri: options ? options.uri : undefined, + purpose: options ? options.purpose : uuid() }; await result.connect(launchInfo, cancelToken); - sendTelemetryEvent(uri ? Telemetry.ConnectRemoteJupyter : Telemetry.ConnectLocalJupyter); + sendTelemetryEvent(launchInfo.uri ? Telemetry.ConnectRemoteJupyter : Telemetry.ConnectLocalJupyter); return result; } catch (err) { // Something else went wrong @@ -192,6 +199,11 @@ export class JupyterExecutionBase implements IJupyterExecution { return result.stdout; } + public getServer(options?: INotebookServerOptions) : Promise { + // This is cached at the host or guest level + return Promise.resolve(undefined); + } + protected async getMatchingKernelSpec(connection?: IConnection, cancelToken?: CancellationToken): Promise { // If not using an active connection, check on disk if (!connection) { diff --git a/src/client/datascience/jupyter/jupyterExecutionFactory.ts b/src/client/datascience/jupyter/jupyterExecutionFactory.ts index c94a042e9d10..20b03413b9c3 100644 --- a/src/client/datascience/jupyter/jupyterExecutionFactory.ts +++ b/src/client/datascience/jupyter/jupyterExecutionFactory.ts @@ -7,10 +7,22 @@ import { CancellationToken } from 'vscode'; import { ILiveShareApi, IWorkspaceService } from '../../common/application/types'; import { IFileSystem } from '../../common/platform/types'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../../common/process/types'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, ILogger } from '../../common/types'; +import { + IAsyncDisposable, + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + ILogger +} from '../../common/types'; import { IInterpreterService, IKnownSearchPathsForInterpreters, PythonInterpreter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { IJupyterCommandFactory, IJupyterExecution, IJupyterSessionManager, INotebookServer } from '../types'; +import { + IJupyterCommandFactory, + IJupyterExecution, + IJupyterSessionManager, + INotebookServer, + INotebookServerOptions +} from '../types'; import { GuestJupyterExecution } from './liveshare/guestJupyterExecution'; import { HostJupyterExecution } from './liveshare/hostJupyterExecution'; import { IRoleBasedObject, RoleBasedFactory } from './liveshare/roleBasedFactory'; @@ -37,7 +49,7 @@ type JupyterExecutionClassType = { }; @injectable() -export class JupyterExecutionFactory implements IJupyterExecution { +export class JupyterExecutionFactory implements IJupyterExecution, IAsyncDisposable { private executionFactory: RoleBasedFactory; @@ -55,6 +67,7 @@ export class JupyterExecutionFactory implements IJupyterExecution { @inject(IConfigurationService) configuration: IConfigurationService, @inject(IJupyterCommandFactory) commandFactory : IJupyterCommandFactory, @inject(IServiceContainer) serviceContainer: IServiceContainer) { + asyncRegistry.push(this); this.executionFactory = new RoleBasedFactory( liveShare, HostJupyterExecution, @@ -76,6 +89,12 @@ export class JupyterExecutionFactory implements IJupyterExecution { ); } + public async dispose() : Promise { + // Dispose of our execution object + const execution = await this.executionFactory.get(); + return execution.dispose(); + } + public async isNotebookSupported(cancelToken?: CancellationToken): Promise { const execution = await this.executionFactory.get(); return execution.isNotebookSupported(cancelToken); @@ -92,10 +111,14 @@ export class JupyterExecutionFactory implements IJupyterExecution { const execution = await this.executionFactory.get(); return execution.isKernelSpecSupported(cancelToken); } - public async connectToNotebookServer(uri: string | undefined, usingDarkTheme: boolean, useDefaultConfig: boolean, cancelToken?: CancellationToken, workingDir?: string): Promise { + public async isSpawnSupported(cancelToken?: CancellationToken): Promise { const execution = await this.executionFactory.get(); - return execution.connectToNotebookServer(uri, usingDarkTheme, useDefaultConfig, cancelToken, workingDir); + return execution.isSpawnSupported(cancelToken); } + public async connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken): Promise { + const execution = await this.executionFactory.get(); + return execution.connectToNotebookServer(options, cancelToken); +} public async spawnNotebook(file: string): Promise { const execution = await this.executionFactory.get(); return execution.spawnNotebook(file); @@ -108,8 +131,8 @@ export class JupyterExecutionFactory implements IJupyterExecution { const execution = await this.executionFactory.get(); return execution.getUsableJupyterPython(cancelToken); } - public async dispose(): Promise { + public async getServer(options?: INotebookServerOptions) : Promise { const execution = await this.executionFactory.get(); - return execution.dispose(); + return execution.getServer(options); } } diff --git a/src/client/datascience/jupyter/jupyterServer.ts b/src/client/datascience/jupyter/jupyterServer.ts index f74b89364560..25ef85e5a760 100644 --- a/src/client/datascience/jupyter/jupyterServer.ts +++ b/src/client/datascience/jupyter/jupyterServer.ts @@ -114,6 +114,8 @@ export class JupyterServerBase implements INotebookServer { private sessionStartTime: number | undefined; private pendingCellSubscriptions: CellSubscriber[] = []; private ranInitialSetup = false; + private id = uuid(); + private connectPromise: Deferred = createDeferred(); constructor( liveShare: ILiveShareApi, @@ -126,10 +128,15 @@ export class JupyterServerBase implements INotebookServer { this.asyncRegistry.push(this); } - public async connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken) { + public async connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken) : Promise { + this.logger.logInformation(`Connecting server ${this.id}`); + // Save our launch info this.launchInfo = launchInfo; + // Indicate connect started + this.connectPromise.resolve(launchInfo); + // Start our session this.session = await this.sessionManager.startNew(launchInfo.connectionInfo, launchInfo.kernelSpec, cancelToken); @@ -146,6 +153,7 @@ export class JupyterServerBase implements INotebookServer { } public shutdown(): Promise { + this.logger.logInformation(`Shutting down ${this.id}`); const dispose = this.session ? this.session.dispose() : undefined; return dispose ? dispose : Promise.resolve(); } @@ -333,12 +341,8 @@ export class JupyterServerBase implements INotebookServer { throw new Error(localize.DataScience.sessionDisposed()); } - public getLaunchInfo(): INotebookServerLaunchInfo | undefined { - if (!this.launchInfo) { - return undefined; - } - - return this.launchInfo; + public waitForConnect(): Promise { + return this.connectPromise.promise; } // Return a copy of the connection information that this server used to connect with diff --git a/src/client/datascience/jupyter/jupyterServerFactory.ts b/src/client/datascience/jupyter/jupyterServerFactory.ts index 8690cbe88f1c..0236e41e29c2 100644 --- a/src/client/datascience/jupyter/jupyterServerFactory.ts +++ b/src/client/datascience/jupyter/jupyterServerFactory.ts @@ -130,8 +130,9 @@ export class JupyterServerFactory implements INotebookServer { return undefined; } - public getLaunchInfo(): INotebookServerLaunchInfo | undefined { - return this.launchInfo; + public async waitForConnect(): Promise { + const server = await this.serverFactory.get(); + return server.waitForConnect(); } public async getSysInfo() : Promise { diff --git a/src/client/datascience/jupyter/jupyterServerManager.ts b/src/client/datascience/jupyter/jupyterServerManager.ts deleted file mode 100644 index 4cbb7041a6be..000000000000 --- a/src/client/datascience/jupyter/jupyterServerManager.ts +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; - -import { IWorkspaceService } from '../../common/application/types'; -import { IFileSystem } from '../../common/platform/types'; -import { IAsyncDisposable, IAsyncDisposableRegistry, IConfigurationService } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { IInterpreterService } from '../../interpreter/contracts'; -import { Settings } from '../constants'; -import { IJupyterExecution, INotebookServer, INotebookServerManager, IStatusProvider } from '../types'; - -interface ILaunchParameters { - serverURI: string | undefined; - workingDir: string | undefined; - darkTheme : boolean; - useDefaultConfig : boolean; -} - -@injectable() -export class JupyterServerManager implements INotebookServerManager, IAsyncDisposable { - // Currently coding this as just a single server instance. - // It's encapsulated here so we can add support for multiple servers as needed pretty easily - private activeServer: INotebookServer | undefined; - - constructor( - @inject(IAsyncDisposableRegistry) private asyncRegistry: IAsyncDisposableRegistry, - @inject(IConfigurationService) private configuration: IConfigurationService, - @inject(IInterpreterService) private interpreterService: IInterpreterService, - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, - @inject(IStatusProvider) private statusProvider: IStatusProvider, - @inject(IWorkspaceService) private workspaceService: IWorkspaceService) { - this.asyncRegistry.push(this); - } - - // Either return our current active server or create a new one from our settings if needed - public async getOrCreateServer(): Promise { - // Find the settings that we are going to launch our server with - const launchParameters = await this.getLaunchParameters(); - if (await this.isActiveServer(launchParameters)) { - // If we already have a server of these settings, just return it - return this.activeServer; - } else { - // If not shutdown the old server and start up a new one - if (this.activeServer) { - await this.activeServer.dispose(); - this.activeServer = undefined; - } - - const status = this.statusProvider.set(localize.DataScience.connectingToJupyter()); - - try { - this.activeServer = await this.jupyterExecution.connectToNotebookServer( - launchParameters.serverURI, - launchParameters.darkTheme, - launchParameters.useDefaultConfig, - undefined, - launchParameters.workingDir); - - return this.activeServer; - } finally { - if (status) { - status.dispose(); - } - } - } - } - - public async getServer() : Promise { - // Compute launch parameters. - const launchParameters = await this.getLaunchParameters(); - - if (await this.isActiveServer(launchParameters)) { - // If we already have a server of these settings, just return it - return this.activeServer; - } - } - - // Don't check the launch paramters, just return back the active - // used for components that never create or control the active server like the variables view - public getActiveServer(): INotebookServer | undefined { - return this.activeServer; - } - - public dispose(): Promise { - if (this.activeServer) { - return this.activeServer.dispose(); - } else { - return Promise.resolve(); - } - } - - private async getLaunchParameters() : Promise { - // Find the settings that we are going to launch our server with - const settings = this.configuration.getSettings(); - let serverURI: string | undefined = settings.datascience.jupyterServerURI; - let workingDir: string | undefined; - const useDefaultConfig: boolean | undefined = settings.datascience.useDefaultConfigForJupyter; - // Check for dark theme, if so set matplot lib to use dark_background settings - let darkTheme: boolean = false; - const workbench = this.workspaceService.getConfiguration('workbench'); - if (workbench) { - const theme = workbench.get('colorTheme'); - if (theme) { - darkTheme = /dark/i.test(theme); - } - } - - // For the local case pass in our URI as undefined, that way connect doesn't have to check the setting - if (serverURI === Settings.JupyterServerLocalLaunch) { - serverURI = undefined; - - workingDir = await this.calculateWorkingDirectory(); - } - - return { - serverURI, - workingDir, - useDefaultConfig, - darkTheme - }; - } - - // Given our launch parameters, is this server already the active server? - private async isActiveServer(launchParameters: ILaunchParameters): Promise { - if (!this.activeServer || !this.activeServer.getLaunchInfo()) { - return false; - } - - const launchInfo = this.activeServer.getLaunchInfo(); - - // Check here to see if we have the same settings as a server that we already have running - // Note: we are not looking at the kernel spec here this saves us from having to enumerate - // kernel specs when looking for a similar server, instead we just look if the interpreter is different - // however this could mean that if you add a new kernel spec while a server is running then we won't - // detect that launch could give you a different server in that case - // ! ok as we have already exited if get launch info is undefined - if (launchInfo!.uri === launchParameters.serverURI && launchInfo!.usingDarkTheme === launchParameters.darkTheme - && launchInfo!.workingDir === launchParameters.workingDir) { - const info = await this.interpreterService.getActiveInterpreter(); - if (info === launchInfo!.currentInterpreter) { - return true; - } - } - - return false; - } - - // Calculate the working directory that we should move into when starting up our Jupyter server locally - private async calculateWorkingDirectory(): Promise { - let workingDir: string | undefined; - // For a local launch calculate the working directory that we should switch into - const settings = this.configuration.getSettings(); - const fileRoot = settings.datascience.notebookFileRoot; - - // If we don't have a workspace open the notebookFileRoot seems to often have a random location in it (we use ${workspaceRoot} as default) - // so only do this setting if we actually have a valid workspace open - if (fileRoot && this.workspaceService.hasWorkspaceFolders) { - const workspaceFolderPath = this.workspaceService.workspaceFolders![0].uri.fsPath; - if (path.isAbsolute(fileRoot)) { - if (await this.fileSystem.directoryExists(fileRoot)) { - // User setting is absolute and exists, use it - workingDir = fileRoot; - } else { - // User setting is absolute and doesn't exist, use workspace - workingDir = workspaceFolderPath; - } - } else { - // fileRoot is a relative path, combine it with the workspace folder - const combinedPath = path.join(workspaceFolderPath, fileRoot); - if (await this.fileSystem.directoryExists(combinedPath)) { - // combined path exists, use it - workingDir = combinedPath; - } else { - // Combined path doesn't exist, use workspace - workingDir = workspaceFolderPath; - } - } - } - return workingDir; - } -} diff --git a/src/client/datascience/jupyter/jupyterVariables.ts b/src/client/datascience/jupyter/jupyterVariables.ts index f5ece8869f91..ae1f5f83f000 100644 --- a/src/client/datascience/jupyter/jupyterVariables.ts +++ b/src/client/datascience/jupyter/jupyterVariables.ts @@ -1,17 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - 'use strict'; import { nbformat } from '@jupyterlab/coreutils'; import { inject, injectable } from 'inversify'; import * as path from 'path'; import * as uuid from 'uuid/v4'; + import { IFileSystem } from '../../common/platform/types'; import * as localize from '../../common/utils/localize'; import { EXTENSION_ROOT_DIR } from '../../constants'; import { Identifiers } from '../constants'; -import { ICell, IJupyterVariable, IJupyterVariables, INotebookServerManager } from '../types'; +import { ICell, IHistoryProvider, IJupyterExecution, IJupyterVariable, IJupyterVariables } from '../types'; @injectable() export class JupyterVariables implements IJupyterVariables { @@ -19,7 +19,8 @@ export class JupyterVariables implements IJupyterVariables { constructor( @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(INotebookServerManager) private jupyterServerManager: INotebookServerManager + @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, + @inject(IHistoryProvider) private historyProvider: IHistoryProvider ) { } @@ -30,7 +31,7 @@ export class JupyterVariables implements IJupyterVariables { await this.loadVariablesFile(); } - const activeServer = this.jupyterServerManager.getActiveServer(); + const activeServer = await this.jupyterExecution.getServer(await this.historyProvider.getNotebookOptions()); if (!activeServer) { // No active server will just return an empty list return []; diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts b/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts index b498fc154152..ba0ab2a390c7 100644 --- a/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts +++ b/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. 'use strict'; import { injectable } from 'inversify'; +import * as uuid from 'uuid/v4'; import { CancellationToken } from 'vscode'; import { ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; @@ -12,14 +13,22 @@ import * as localize from '../../../common/utils/localize'; import { IInterpreterService, IKnownSearchPathsForInterpreters, PythonInterpreter } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; import { LiveShare, LiveShareCommands } from '../../constants'; -import { IConnection, IJupyterCommandFactory, IJupyterSessionManager, INotebookServer } from '../../types'; +import { + IConnection, + IJupyterCommandFactory, + IJupyterSessionManager, + INotebookServer, + INotebookServerOptions +} from '../../types'; import { JupyterConnectError } from '../jupyterConnectError'; import { JupyterExecutionBase } from '../jupyterExecution'; import { LiveShareParticipantGuest } from './liveShareParticipantMixin'; +import { ServerCache } from './serverCache'; // This class is really just a wrapper around a jupyter execution that also provides a shared live share service @injectable() export class GuestJupyterExecution extends LiveShareParticipantGuest(JupyterExecutionBase, LiveShare.JupyterExecutionService) { + private serverCache : ServerCache; constructor( liveShare: ILiveShareApi, @@ -52,10 +61,14 @@ export class GuestJupyterExecution extends LiveShareParticipantGuest(JupyterExec commandFactory, serviceContainer); asyncRegistry.push(this); + this.serverCache = new ServerCache(configuration, workspace, fileSystem); } public async dispose() : Promise { await super.dispose(); + + // Dispose of all of our cached servers + await this.serverCache.dispose(); } public async isNotebookSupported(cancelToken?: CancellationToken): Promise { @@ -70,18 +83,38 @@ export class GuestJupyterExecution extends LiveShareParticipantGuest(JupyterExec public isKernelSpecSupported(cancelToken?: CancellationToken): Promise { return this.checkSupported(LiveShareCommands.isKernelSpecSupported, cancelToken); } - public async connectToNotebookServer(uri: string, usingDarkTheme: boolean, useDefaultConfig: boolean, cancelToken?: CancellationToken, workingDir?: string): Promise { - let result: INotebookServer | undefined ; + public isSpawnSupported(cancelToken?: CancellationToken): Promise { + return Promise.resolve(false); + } + public async connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken): Promise { + let result: INotebookServer | undefined = await this.serverCache.get(options); + + // See if we already have this server or not. + if (result) { + return result; + } // Create the server on the remote machine. It should return an IConnection we can use to build a remote uri const service = await this.waitForService(); if (service) { - const connection: IConnection = await service.request(LiveShareCommands.connectToNotebookServer, [usingDarkTheme, useDefaultConfig, workingDir], cancelToken); + const purpose = options ? options.purpose : uuid(); + const connection: IConnection = await service.request( + LiveShareCommands.connectToNotebookServer, + [options], + cancelToken); // If that works, then treat this as a remote server and connect to it if (connection && connection.baseUrl) { const newUri = `${connection.baseUrl}?token=${connection.token}`; - result = await super.connectToNotebookServer(newUri, usingDarkTheme, useDefaultConfig, cancelToken); + result = await super.connectToNotebookServer( + { + uri: newUri, + usingDarkTheme: options && options.usingDarkTheme, + useDefaultConfig: options && options.useDefaultConfig, + workingDir: options ? options.workingDir : undefined, + purpose + }, + cancelToken); } } @@ -95,10 +128,7 @@ export class GuestJupyterExecution extends LiveShareParticipantGuest(JupyterExec // Not supported in liveshare throw new Error(localize.DataScience.liveShareCannotSpawnNotebooks()); } - public importNotebook(file: string, template: string): Promise { - // Not supported in liveshare - throw new Error(localize.DataScience.liveShareCannotImportNotebooks()); - } + public async getUsableJupyterPython(cancelToken?: CancellationToken): Promise { const service = await this.waitForService(); if (service) { @@ -106,6 +136,10 @@ export class GuestJupyterExecution extends LiveShareParticipantGuest(JupyterExec } } + public async getServer(options?: INotebookServerOptions) : Promise { + return this.serverCache.get(options); + } + private async checkSupported(command: string, cancelToken?: CancellationToken) : Promise { const service = await this.waitForService(); diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts b/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts index ce1e6d7406cd..6b459efbfcbd 100644 --- a/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts +++ b/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. 'use strict'; import { Observable } from 'rxjs/Observable'; -import { Subscriber } from 'rxjs/Subscriber'; import { CancellationToken } from 'vscode-jsonrpc'; import * as vsls from 'vsls/vscode'; @@ -22,20 +21,15 @@ import { InterruptResult } from '../../types'; import { LiveShareParticipantDefault, LiveShareParticipantGuest } from './liveShareParticipantMixin'; -import { - IExecuteObservableResponse, - IInterruptResponse, - ILiveShareParticipant, - IServerResponse, - ServerResponseType -} from './types'; +import { ResponseQueue } from './responseQueue'; +import { ILiveShareParticipant, IServerResponse } from './types'; export class GuestJupyterServer extends LiveShareParticipantGuest(LiveShareParticipantDefault, LiveShare.JupyterServerSharedService) implements INotebookServer, ILiveShareParticipant { private launchInfo : INotebookServerLaunchInfo | undefined; - private responseQueue : IServerResponse [] = []; - private waitingQueue : { deferred: Deferred; predicate(r: IServerResponse) : boolean }[] = []; + private responseQueue : ResponseQueue = new ResponseQueue(); + private connectPromise: Deferred = createDeferred(); constructor( liveShare: ILiveShareApi, @@ -43,22 +37,27 @@ export class GuestJupyterServer logger: ILogger, private disposableRegistry: IDisposableRegistry, asyncRegistry: IAsyncDisposableRegistry, - configService: IConfigurationService, + private configService: IConfigurationService, sessionManager: IJupyterSessionManager) { super(liveShare); } public async connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken): Promise { this.launchInfo = launchInfo; + this.connectPromise.resolve(launchInfo); return Promise.resolve(); } - public shutdown(): Promise { - return Promise.resolve(); + public async shutdown(): Promise { + // Send this across to the other side. Otherwise the host server will remain running. + const service = await this.waitForService(); + if (service) { + service.notify(LiveShareCommands.disposeServer, {}); + } } public dispose(): Promise { - return Promise.resolve(); + return this.shutdown(); } public waitForIdle(): Promise { @@ -98,24 +97,26 @@ export class GuestJupyterServer } public executeObservable(code: string, file: string, line: number, id: string): Observable { - // Create a wrapper observable around the actual server - return new Observable(subscriber => { - // Wait for the observable responses to come in - this.waitForObservable(subscriber, code, file, line, id) - .catch(e => { - subscriber.error(e); - subscriber.complete(); - }); - }); + // Mimic this to the other side and then wait for a response + this.waitForService().then(s => { + if (s) { + s.notify(LiveShareCommands.executeObservable, { code, file, line, id }); + } + }).ignoreErrors(); + return this.responseQueue.waitForObservable(code, file, line, id); } public async restartKernel(): Promise { - await this.waitForResponse(ServerResponseType.Restart); + // We need to force a restart on the host side + return this.sendRequest(LiveShareCommands.restart, []); } public async interruptKernel(timeoutMs: number): Promise { - const response = await this.waitForResponse(ServerResponseType.Restart); - return (response as IInterruptResponse).result; + const settings = this.configService.getSettings(); + const interruptTimeout = settings.datascience.jupyterInterruptTimeout; + + const response = await this.sendRequest(LiveShareCommands.interrupt, [interruptTimeout]); + return (response as InterruptResult); } // Return a copy of the connection information that this server used to connect with @@ -127,8 +128,16 @@ export class GuestJupyterServer return undefined; } - public getLaunchInfo(): INotebookServerLaunchInfo | undefined { - return this.launchInfo; + public waitForConnect(): Promise { + return this.connectPromise.promise; + } + + public async waitForServiceName() : Promise { + // First wait for connect to occur + const launchInfo = await this.waitForConnect(); + + // Use our base name plus our purpose. This means one unique server per purpose + return LiveShare.JupyterServerSharedService + (launchInfo ? launchInfo.purpose : ''); } public async getSysInfo() : Promise { @@ -164,71 +173,15 @@ export class GuestJupyterServer // Args should be of type ServerResponse. Stick in our queue if so. if (args.hasOwnProperty('type')) { this.responseQueue.push(args as IServerResponse); - - // Check for any waiters. - this.dispatchResponses(); } } - private async waitForObservable(subscriber: Subscriber, code: string, file: string, line: number, id: string) : Promise { - let pos = 0; - let foundId = id; - let cells: ICell[] | undefined = []; - while (cells !== undefined) { - // Find all matches in order - const response = await this.waitForSpecificResponse(r => { - return (r.pos === pos) && - (foundId === r.id || !foundId) && - (code === r.code) && - (!r.cells || (r.cells && r.cells[0].file === file && r.cells[0].line === line)); - }); - if (response.cells) { - subscriber.next(response.cells); - pos += 1; - foundId = response.id; - } - cells = response.cells; - } - subscriber.complete(); - } - - private waitForSpecificResponse(predicate: (response: T) => boolean) : Promise { - // See if we have any responses right now with this type - const index = this.responseQueue.findIndex(r => predicate(r as T)); - if (index >= 0) { - // Pull off the match - const match = this.responseQueue[index]; - - // Remove from the response queue every response before this one as we're not going - // to be asking for them anymore. (they should be old requests) - this.responseQueue = this.responseQueue.length > index + 1 ? this.responseQueue.slice(index + 1) : []; - - // Return this single item - return Promise.resolve(match as T); - } else { - // We have to wait for a new input to happen - const waitable = { deferred: createDeferred(), predicate }; - this.waitingQueue.push(waitable); - return waitable.deferred.promise; + // tslint:disable-next-line:no-any + private async sendRequest(command: string, args: any[]) : Promise { + const service = await this.waitForService(); + if (service) { + return service.request(command, args); } } - private waitForResponse(type: ServerResponseType) : Promise { - return this.waitForSpecificResponse(r => r.type === type); - } - - private dispatchResponses() { - // Look through all of our responses that are queued up and see if they make a - // waiting promise resolve - for (let i = 0; i < this.responseQueue.length; i += 1) { - const response = this.responseQueue[i]; - const matchIndex = this.waitingQueue.findIndex(w => w.predicate(response)); - if (matchIndex >= 0) { - this.waitingQueue[matchIndex].deferred.resolve(response); - this.waitingQueue.splice(matchIndex, 1); - this.responseQueue.splice(i, 1); - i -= 1; // Offset the addition as we removed this item - } - } - } } diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts b/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts index 2eb90a79292e..9d662f4a9b2f 100644 --- a/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts +++ b/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts @@ -3,29 +3,29 @@ 'use strict'; import '../../../common/extensions'; -import * as os from 'os'; -import { CancellationToken, Disposable } from 'vscode'; +import { CancellationToken } from 'vscode'; import * as vsls from 'vsls/vscode'; import { ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; import { IFileSystem } from '../../../common/platform/types'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../../../common/process/types'; import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, ILogger } from '../../../common/types'; -import * as localize from '../../../common/utils/localize'; import { noop } from '../../../common/utils/misc'; import { IInterpreterService, IKnownSearchPathsForInterpreters } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; -import { LiveShare, LiveShareCommands, RegExpValues } from '../../constants'; +import { LiveShare, LiveShareCommands } from '../../constants'; import { IConnection, IJupyterCommandFactory, IJupyterExecution, IJupyterSessionManager, - INotebookServer + INotebookServer, + INotebookServerOptions } from '../../types'; import { JupyterExecutionBase } from '../jupyterExecution'; import { LiveShareParticipantHost } from './liveShareParticipantMixin'; import { IRoleBasedObject } from './roleBasedFactory'; +import { ServerCache } from './serverCache'; // tslint:disable:no-any @@ -33,9 +33,7 @@ import { IRoleBasedObject } from './roleBasedFactory'; export class HostJupyterExecution extends LiveShareParticipantHost(JupyterExecutionBase, LiveShare.JupyterExecutionService) implements IRoleBasedObject, IJupyterExecution { - private sharedServers: Disposable [] = []; - private fowardedPorts: number [] = []; - private runningServer: INotebookServer | undefined; + private serverCache : ServerCache; constructor( liveShare: ILiveShareApi, executionFactory: IPythonExecutionFactory, @@ -45,10 +43,10 @@ export class HostJupyterExecution logger: ILogger, disposableRegistry: IDisposableRegistry, asyncRegistry: IAsyncDisposableRegistry, - fileSystem: IFileSystem, + fileSys: IFileSystem, sessionManager: IJupyterSessionManager, workspace: IWorkspaceService, - configuration: IConfigurationService, + configService: IConfigurationService, commandFactory : IJupyterCommandFactory, serviceContainer: IServiceContainer) { super( @@ -60,63 +58,36 @@ export class HostJupyterExecution logger, disposableRegistry, asyncRegistry, - fileSystem, + fileSys, sessionManager, workspace, - configuration, + configService, commandFactory, serviceContainer); + this.serverCache = new ServerCache(configService, workspace, fileSys); } public async dispose() : Promise { await super.dispose(); const api = await this.api; await this.onDetach(api); - this.fowardedPorts = []; } - public async connectToNotebookServer(uri: string | undefined, usingDarkTheme: boolean, useDefaultConfig: boolean, cancelToken?: CancellationToken, workingDir?: string): Promise { - // We only have a single server at a time. - if (!this.runningServer) { - + public async connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken): Promise { + // See if we have this server in our cache already or not + let result = await this.serverCache.get(options); + if (result) { + return result; + } else { // Create the server - let sharedServerDisposable : Disposable | undefined; - const result = await super.connectToNotebookServer(uri, usingDarkTheme, useDefaultConfig, cancelToken, workingDir); - - // Then using the liveshare api, port forward whatever port is being used by the server - - // tslint:disable-next-line:no-suspicious-comment - // TODO: Liveshare can actually change this value on the guest. So on the guest side we need to listen - // to an event they are going to add to their api - if (!uri && result) { - const connectionInfo = result.getConnectionInfo(); - if (connectionInfo) { - const portMatch = RegExpValues.ExtractPortRegex.exec(connectionInfo.baseUrl); - if (portMatch && portMatch.length > 1) { - sharedServerDisposable = await this.portForwardServer(parseInt(portMatch[1], 10)); - } - } - } + result = await super.connectToNotebookServer(await this.serverCache.generateDefaultOptions(options), cancelToken); + // Save in our cache if (result) { - // Save this result, but modify its dispose such that we - // can detach from the server when it goes away. - this.runningServer = result; - const oldDispose = result.dispose.bind(result); - result.dispose = () => { - // Dispose of the shared server - if (sharedServerDisposable) { - sharedServerDisposable.dispose(); - } - // Mark as not having a running server anymore - this.runningServer = undefined; - - return oldDispose(); - }; + await this.serverCache.set(result, noop, options); } + return result; } - - return this.runningServer; } public async onAttach(api: vsls.LiveShare | null) : Promise { @@ -132,37 +103,19 @@ export class HostJupyterExecution service.onRequest(LiveShareCommands.connectToNotebookServer, this.onRemoteConnectToNotebookServer); service.onRequest(LiveShareCommands.getUsableJupyterPython, this.onRemoteGetUsableJupyterPython); } - - // Port forward all of the servers that need it - this.fowardedPorts.forEach(p => this.portForwardServer(p).ignoreErrors()); } } public async onDetach(api: vsls.LiveShare | null) : Promise { - if (api) { - await api.unshareService(LiveShare.JupyterExecutionService); - } + await super.onDetach(api); - // Unshare all of our port forwarded servers - this.sharedServers.forEach(s => s.dispose()); - this.sharedServers = []; + // clear our cached servers. We need to reconnect + await this.serverCache.dispose(); } - private async portForwardServer(port: number) : Promise { - // Share this port with all guests if we are actively in a session. Otherwise save for when we are. - let result : Disposable | undefined; - const api = await this.api; - if (api && api.session && api.session.role === vsls.Role.Host) { - result = await api.shareServer({port, displayName: localize.DataScience.liveShareHostFormat().format(os.hostname())}); - this.sharedServers.push(result!); - } - - // Save for reattaching if necessary later - if (this.fowardedPorts.indexOf(port) === -1) { - this.fowardedPorts.push(port); - } - - return result; + public getServer(options?: INotebookServerOptions) : Promise { + // See if we have this server or not. + return this.serverCache.get(options); } private onRemoteIsNotebookSupported = (args: any[], cancellation: CancellationToken): Promise => { @@ -186,7 +139,7 @@ export class HostJupyterExecution private onRemoteConnectToNotebookServer = async (args: any[], cancellation: CancellationToken): Promise => { // Connect to the local server. THe local server should have started the port forwarding already - const localServer = await this.connectToNotebookServer(undefined, args[0], args[1], cancellation, args[2]); + const localServer = await this.connectToNotebookServer(args[0] as INotebookServerOptions | undefined, cancellation); // Extract the URI and token for the other side if (localServer) { diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts index 359cc437c784..68a58e067726 100644 --- a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts +++ b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts @@ -3,6 +3,7 @@ 'use strict'; import '../../../common/extensions'; +import * as os from 'os'; import { Observable } from 'rxjs/Observable'; import * as vscode from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; @@ -10,10 +11,20 @@ import * as vsls from 'vsls/vscode'; import { ILiveShareApi } from '../../../common/application/types'; import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, ILogger } from '../../../common/types'; -import { Identifiers, LiveShare, LiveShareCommands } from '../../constants'; -import { ICell, IDataScience, IJupyterSessionManager, INotebookServer, InterruptResult } from '../../types'; +import * as localize from '../../../common/utils/localize'; +import { Identifiers, LiveShare, LiveShareCommands, RegExpValues } from '../../constants'; +import { IExecuteInfo } from '../../historyTypes'; +import { + ICell, + IDataScience, + IJupyterSessionManager, + INotebookServer, + INotebookServerLaunchInfo, + InterruptResult +} from '../../types'; import { JupyterServerBase } from '../jupyterServer'; import { LiveShareParticipantHost } from './liveShareParticipantMixin'; +import { ResponseQueue } from './responseQueue'; import { IRoleBasedObject } from './roleBasedFactory'; import { IResponseMapping, IServerResponse, ServerResponseType } from './types'; @@ -22,8 +33,12 @@ import { IResponseMapping, IServerResponse, ServerResponseType } from './types'; export class HostJupyterServer extends LiveShareParticipantHost(JupyterServerBase, LiveShare.JupyterServerSharedService) implements IRoleBasedObject, INotebookServer { - private responseBacklog : IServerResponse[] = []; + private responseQueue : ResponseQueue = new ResponseQueue(); + private requestLog : Map = new Map(); private catchupPendingCount : number = 0; + private disposed = false; + private portToForward = 0; + private sharedPort : vscode.Disposable | undefined; constructor( liveShare: ILiveShareApi, dataScience: IDataScience, @@ -36,30 +51,66 @@ export class HostJupyterServer } public async dispose(): Promise { - await super.dispose(); - const api = await this.api; - return this.onDetach(api) ; + if (!this.disposed) { + this.disposed = true; + await super.dispose(); + const api = await this.api; + return this.onDetach(api) ; + } } - public async onDetach(api: vsls.LiveShare | null) : Promise { - if (api) { - return api.unshareService(LiveShare.JupyterServerSharedService); + public async connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken): Promise { + if (launchInfo.connectionInfo && launchInfo.connectionInfo.localLaunch) { + const portMatch = RegExpValues.ExtractPortRegex.exec(launchInfo.connectionInfo.baseUrl); + if (portMatch && portMatch.length > 1) { + const port = parseInt(portMatch[1], 10); + await this.attemptToForwardPort(this.finishedApi, port); + } } + return super.connect(launchInfo, cancelToken); } public async onAttach(api: vsls.LiveShare | null) : Promise { - if (api) { + if (api && !this.disposed) { const service = await this.waitForService(); // Attach event handlers to different requests if (service) { - service.onRequest(LiveShareCommands.syncRequest, (args: object, cancellation: CancellationToken) => this.onSync()); - service.onRequest(LiveShareCommands.getSysInfo, (args: any[], cancellation: CancellationToken) => this.onGetSysInfoRequest(cancellation)); + // Requests return arrays + service.onRequest(LiveShareCommands.syncRequest, (args: any[], cancellation: CancellationToken) => this.onSync()); + service.onRequest(LiveShareCommands.getSysInfo, (args: any[], cancellation: CancellationToken) => this.onGetSysInfoRequest(cancellation)); + service.onRequest(LiveShareCommands.restart, (args: any[], cancellation: CancellationToken) => this.onRestartRequest(cancellation)); + service.onRequest(LiveShareCommands.interrupt, (args: any[], cancellation: CancellationToken) => this.onInterruptRequest(args.length > 0 ? args[0] as number : LiveShare.InterruptDefaultTimeout, cancellation)); + + // Notifications are always objects. service.onNotify(LiveShareCommands.catchupRequest, (args: object) => this.onCatchupRequest(args)); + service.onNotify(LiveShareCommands.executeObservable, (args: object) => this.onExecuteObservableRequest(args)); + service.onNotify(LiveShareCommands.disposeServer, (args: object) => this.dispose().ignoreErrors()); + + // See if we need to forward the port + await this.attemptToForwardPort(api, this.portToForward); } } } + public async onDetach(api: vsls.LiveShare | null) : Promise { + await super.onDetach(api); + + // Make sure to unshare our port + if (api && this.sharedPort) { + this.sharedPort.dispose(); + this.sharedPort = undefined; + } + } + + public async waitForServiceName() : Promise { + // First wait for connect to occur + const launchInfo = await this.waitForConnect(); + + // Use our base name plus our purpose. This means one unique server per purpose + return LiveShare.JupyterServerSharedService + (launchInfo ? launchInfo.purpose : ''); + } + public async onPeerChange(ev: vsls.PeersChangeEvent) : Promise { // Keep track of the number of guests that need to do a catchup request this.catchupPendingCount += @@ -69,11 +120,26 @@ export class HostJupyterServer public executeObservable(code: string, file: string, line: number, id: string): Observable { try { - const inner = super.executeObservable(code, file, line, id); + // See if this has already been asked for not + if (this.requestLog.has(id)) { + // Create a dummy observable out of the responses as they come in. + return this.responseQueue.waitForObservable(code, file, line, id); + } else { + // Otherwise save this request + this.requestLog.set(id, Date.now()); + const inner = super.executeObservable(code, file, line, id); - // Wrap the observable returned so we can listen to it too - return this.wrapObservableResult(code, inner, id); + // Cleanup old requests + const now = Date.now(); + for (const [k, val] of this.requestLog) { + if (now - val > LiveShare.ResponseLifetime) { + this.requestLog.delete(k); + } + } + // Wrap the observable returned so we can listen to it too + return this.wrapObservableResult(code, inner, id); + } } catch (exc) { this.postException(exc); throw exc; @@ -83,9 +149,7 @@ export class HostJupyterServer public async restartKernel(): Promise { try { - const time = Date.now(); await super.restartKernel(); - return this.postResult(ServerResponseType.Restart, {type: ServerResponseType.Restart, time}); } catch (exc) { this.postException(exc); throw exc; @@ -94,16 +158,22 @@ export class HostJupyterServer public async interruptKernel(timeoutMs: number): Promise { try { - const time = Date.now(); - const result = await super.interruptKernel(timeoutMs); - this.postResult(ServerResponseType.Interrupt, {type: ServerResponseType.Interrupt, time, result}); - return result; + return super.interruptKernel(timeoutMs); } catch (exc) { this.postException(exc); throw exc; } } + private async attemptToForwardPort(api: vsls.LiveShare | null | undefined, port: number) : Promise { + if (port !== 0 && api && api.session && api.session.role === vsls.Role.Host) { + this.portToForward = 0; + this.sharedPort = await api.shareServer({port, displayName: localize.DataScience.liveShareHostFormat().format(os.hostname())}); + } else { + this.portToForward = port; + } + } + private translateCellForGuest(cell: ICell) : ICell { const copy = {...cell}; if (this.role === vsls.Role.Host && this.finishedApi && copy.file !== Identifiers.EmptyFileName) { @@ -121,24 +191,47 @@ export class HostJupyterServer return super.getSysInfo(); } + private onRestartRequest(cancellation: CancellationToken) : Promise { + // Just call the base + return super.restartKernel(); + } + private onInterruptRequest(timeout: number, cancellation: CancellationToken) : Promise { + // Just call the base + return super.interruptKernel(timeout); + } + private async onCatchupRequest(args: object) : Promise { if (args.hasOwnProperty('since')) { const service = await this.waitForService(); if (service) { // Send results for all responses that are left. - this.responseBacklog.forEach(r => { - service.notify(LiveShareCommands.serverResponse, r); - }); + this.responseQueue.send(service); // Eliminate old responses if possible. this.catchupPendingCount -= 1; if (this.catchupPendingCount <= 0) { - this.responseBacklog = []; + this.responseQueue.clear(); } } } } + private onExecuteObservableRequest(args: object) { + // See if we started this execute or not already. + if (args.hasOwnProperty('code')) { + const obj = args as IExecuteInfo; + if (!this.requestLog.has(obj.id)) { + // Convert the file name + const uri = vscode.Uri.parse(`vsls:${obj.file}`); + const file = this.finishedApi ? this.finishedApi.convertSharedUriToLocal(uri).fsPath : obj.file; + + // Just call the execute. Locally we won't listen, but if an actual call comes in for the same + // request, it will use the saved responses. + this.execute(obj.code, file, obj.line, obj.id).ignoreErrors(); + } + } + } + private wrapObservableResult(code: string, observable: Observable, id: string) : Observable { return new Observable(subscriber => { let pos = 0; @@ -191,7 +284,7 @@ export class HostJupyterServer }).ignoreErrors(); // Need to also save in memory for those guests that are in the middle of starting up - this.responseBacklog.push(typedResult); + this.responseQueue.push(typedResult); } } } diff --git a/src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts b/src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts index ba0ec38c2abb..8cbac9db1021 100644 --- a/src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts +++ b/src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts @@ -69,6 +69,7 @@ function LiveShareParticipantMixin, S>( private actualRole = vsls.Role.None; private wantedRole = expectedRole; private servicePromise: Promise | undefined; + private serviceFullName: string | undefined; constructor(...rest: any[]) { super(...rest); @@ -97,8 +98,16 @@ function LiveShareParticipantMixin, S>( noop(); } - public async onDetach(api: vsls.LiveShare | null) : Promise { - noop(); + public waitForServiceName() : Promise { + // Default is just to return the server name + return Promise.resolve(serviceName); + } + + public onDetach(api: vsls.LiveShare | null) : Promise { + if (api && this.serviceFullName && api.session && api.session.role === vsls.Role.Host) { + return api.unshareService(this.serviceFullName); + } + return Promise.resolve(); } public async onSessionChange(api: vsls.LiveShare | null) : Promise { @@ -123,7 +132,8 @@ function LiveShareParticipantMixin, S>( if (!api || (api.session.role !== this.wantedRole)) { this.servicePromise = Promise.resolve(undefined); } else { - this.servicePromise = serviceWaiter(api, serviceName); + this.serviceFullName = await this.waitForServiceName(); + this.servicePromise = serviceWaiter(api, this.serviceFullName); } return this.servicePromise; diff --git a/src/client/datascience/jupyter/liveshare/responseQueue.ts b/src/client/datascience/jupyter/liveshare/responseQueue.ts new file mode 100644 index 000000000000..ea8f3f6235dd --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/responseQueue.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Observable } from 'rxjs/Observable'; +import { Subscriber } from 'rxjs/Subscriber'; +import * as vsls from 'vsls/vscode'; + +import { createDeferred, Deferred } from '../../../common/utils/async'; +import { LiveShareCommands } from '../../constants'; +import { ICell } from '../../types'; +import { IExecuteObservableResponse, IServerResponse } from './types'; + +export class ResponseQueue { + private responseQueue : IServerResponse [] = []; + private waitingQueue : { deferred: Deferred; predicate(r: IServerResponse) : boolean }[] = []; + + public waitForObservable(code: string, file: string, line: number, id: string) : Observable { + // Create a wrapper observable around the actual server + return new Observable(subscriber => { + // Wait for the observable responses to come in + this.waitForResponses(subscriber, code, file, line, id) + .catch(e => { + subscriber.error(e); + subscriber.complete(); + }); + }); + } + + public push(response: IServerResponse) { + this.responseQueue.push(response); + this.dispatchResponses(); + } + + public send(service: vsls.SharedService) { + this.responseQueue.forEach(r => service.notify(LiveShareCommands.serverResponse, r)); + } + + public clear() { + this.responseQueue = []; + } + + private async waitForResponses(subscriber: Subscriber, code: string, file: string, line: number, id: string) : Promise { + let pos = 0; + let foundId = id; + let cells: ICell[] | undefined = []; + while (cells !== undefined) { + // Find all matches in order + const response = await this.waitForSpecificResponse(r => { + return (r.pos === pos) && + (foundId === r.id || !foundId) && + (code === r.code) && + (!r.cells || (r.cells && r.cells[0].file === file && r.cells[0].line === line)); + }); + if (response.cells) { + subscriber.next(response.cells); + pos += 1; + foundId = response.id; + } + cells = response.cells; + } + subscriber.complete(); + } + + private waitForSpecificResponse(predicate: (response: T) => boolean) : Promise { + // See if we have any responses right now with this type + const index = this.responseQueue.findIndex(r => predicate(r as T)); + if (index >= 0) { + // Pull off the match + const match = this.responseQueue[index]; + + // Remove from the response queue every response before this one as we're not going + // to be asking for them anymore. (they should be old requests) + this.responseQueue = this.responseQueue.length > index + 1 ? this.responseQueue.slice(index + 1) : []; + + // Return this single item + return Promise.resolve(match as T); + } else { + // We have to wait for a new input to happen + const waitable = { deferred: createDeferred(), predicate }; + this.waitingQueue.push(waitable); + return waitable.deferred.promise; + } + } + + private dispatchResponses() { + // Look through all of our responses that are queued up and see if they make a + // waiting promise resolve + for (let i = 0; i < this.responseQueue.length; i += 1) { + const response = this.responseQueue[i]; + const matchIndex = this.waitingQueue.findIndex(w => w.predicate(response)); + if (matchIndex >= 0) { + this.waitingQueue[matchIndex].deferred.resolve(response); + this.waitingQueue.splice(matchIndex, 1); + this.responseQueue.splice(i, 1); + i -= 1; // Offset the addition as we removed this item + } + } + } +} diff --git a/src/client/datascience/jupyter/liveshare/roleBasedFactory.ts b/src/client/datascience/jupyter/liveshare/roleBasedFactory.ts index 1a02928647a3..d6c84fe61799 100644 --- a/src/client/datascience/jupyter/liveshare/roleBasedFactory.ts +++ b/src/client/datascience/jupyter/liveshare/roleBasedFactory.ts @@ -54,8 +54,10 @@ export class RoleBasedFactory { + objDisposed = true; this.createPromise = undefined; return oldDispose(); }; @@ -72,10 +74,14 @@ export class RoleBasedFactory { - obj.onPeerChange(e).ignoreErrors(); + if (!objDisposed) { + obj.onPeerChange(e).ignoreErrors(); + } }); } diff --git a/src/client/datascience/jupyter/liveshare/serverCache.ts b/src/client/datascience/jupyter/liveshare/serverCache.ts new file mode 100644 index 000000000000..286eec9e4f27 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/serverCache.ts @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../../common/extensions'; + +import * as path from 'path'; +import * as uuid from 'uuid/v4'; + +import { IWorkspaceService } from '../../../common/application/types'; +import { IFileSystem } from '../../../common/platform/types'; +import { IAsyncDisposable, IConfigurationService } from '../../../common/types'; +import { INotebookServer, INotebookServerOptions } from '../../types'; + +export class ServerCache implements IAsyncDisposable { + private cache : Map = new Map(); + private emptyKey = uuid(); + + constructor( + private configService: IConfigurationService, + private workspace: IWorkspaceService, + private fileSystem: IFileSystem + ) {} + + public async get(options?: INotebookServerOptions) : Promise { + const fixedOptions = await this.generateDefaultOptions(options); + const key = this.generateKey(fixedOptions); + if (this.cache.has(key)) { + return this.cache.get(key); + } + } + + public async set(result: INotebookServer, disposeCallback: () => void, options?: INotebookServerOptions) : Promise { + const fixedOptions = await this.generateDefaultOptions(options); + const key = this.generateKey(fixedOptions); + + // Eliminate any already with this key + const item = this.cache.get(key); + if (item) { + await item.dispose(); + } + + // Save in our cache. + this.cache.set(key, result); + + // Save this result, but modify its dispose such that we + // can detach from the server when it goes away. + const oldDispose = result.dispose.bind(result); + result.dispose = () => { + this.cache.delete(key); + disposeCallback(); + return oldDispose(); + }; + } + + public async dispose() : Promise { + // tslint:disable-next-line:no-unused-variable + for (const [k, s] of this.cache) { + await s.dispose(); + } + this.cache.clear(); + } + + public async generateDefaultOptions(options? : INotebookServerOptions) : Promise { + return { + uri: options ? options.uri : undefined, + useDefaultConfig : options ? options.useDefaultConfig : true, // Default for this is true. + usingDarkTheme : options ? options.usingDarkTheme : undefined, + purpose : options ? options.purpose : uuid(), + workingDir : options && options.workingDir ? options.workingDir : await this.calculateWorkingDirectory() + }; + } + + private generateKey(options?: INotebookServerOptions) : string { + if (!options) { + return this.emptyKey; + } else { + // combine all the values together to make a unique key + return options.purpose + + (options.uri ? options.uri : '') + + (options.useDefaultConfig ? 'true' : 'false') + + (options.usingDarkTheme ? 'true' : 'false') + // Ideally we'd have different results for different themes. Not sure how to handle this. + (options.workingDir); + } + } + + private async calculateWorkingDirectory(): Promise { + let workingDir: string | undefined; + // For a local launch calculate the working directory that we should switch into + const settings = this.configService.getSettings(); + const fileRoot = settings.datascience.notebookFileRoot; + + // If we don't have a workspace open the notebookFileRoot seems to often have a random location in it (we use ${workspaceRoot} as default) + // so only do this setting if we actually have a valid workspace open + if (fileRoot && this.workspace.hasWorkspaceFolders) { + const workspaceFolderPath = this.workspace.workspaceFolders![0].uri.fsPath; + if (path.isAbsolute(fileRoot)) { + if (await this.fileSystem.directoryExists(fileRoot)) { + // User setting is absolute and exists, use it + workingDir = fileRoot; + } else { + // User setting is absolute and doesn't exist, use workspace + workingDir = workspaceFolderPath; + } + } else { + // fileRoot is a relative path, combine it with the workspace folder + const combinedPath = path.join(workspaceFolderPath, fileRoot); + if (await this.fileSystem.directoryExists(combinedPath)) { + // combined path exists, use it + workingDir = combinedPath; + } else { + // Combined path doesn't exist, use workspace + workingDir = workspaceFolderPath; + } + } + } + return workingDir; + } + +} diff --git a/src/client/datascience/jupyter/liveshare/types.ts b/src/client/datascience/jupyter/liveshare/types.ts index 21f8587c3833..bb454a413493 100644 --- a/src/client/datascience/jupyter/liveshare/types.ts +++ b/src/client/datascience/jupyter/liveshare/types.ts @@ -4,14 +4,12 @@ import * as vsls from 'vsls/vscode'; import { IAsyncDisposable } from '../../../common/types'; -import { ICell, InterruptResult } from '../../types'; +import { ICell } from '../../types'; // tslint:disable:max-classes-per-file export enum ServerResponseType { ExecuteObservable, - Interrupt, - Restart, Exception } @@ -27,13 +25,6 @@ export interface IExecuteObservableResponse extends IServerResponse { cells: ICell[] | undefined; } -export interface IInterruptResponse extends IServerResponse { - result: InterruptResult; -} - -export interface IRestartResponse extends IServerResponse { -} - export interface IExceptionResponse extends IServerResponse { message: string; } @@ -41,8 +32,6 @@ export interface IExceptionResponse extends IServerResponse { // Map all responses to their properties export interface IResponseMapping { [ServerResponseType.ExecuteObservable]: IExecuteObservableResponse; - [ServerResponseType.Interrupt]: IInterruptResponse; - [ServerResponseType.Restart]: IRestartResponse; [ServerResponseType.Exception]: IExceptionResponse; } @@ -56,4 +45,5 @@ export interface ILiveShareParticipant extends IAsyncDisposable { onAttach(api: vsls.LiveShare | null) : Promise; onDetach(api: vsls.LiveShare | null) : Promise; onPeerChange(ev: vsls.PeersChangeEvent) : Promise; + waitForServiceName() : Promise; } diff --git a/src/client/datascience/liveshare/postOffice.ts b/src/client/datascience/liveshare/postOffice.ts index 69d5fe580e41..2fa1eb41d91d 100644 --- a/src/client/datascience/liveshare/postOffice.ts +++ b/src/client/datascience/liveshare/postOffice.ts @@ -2,13 +2,12 @@ // Licensed under the MIT License. 'use strict'; import { JSONArray } from '@phosphor/coreutils'; -import * as uuid from 'uuid/v4'; import * as vscode from 'vscode'; import * as vsls from 'vsls/vscode'; import { ILiveShareApi } from '../../common/application/types'; import { IAsyncDisposable } from '../../common/types'; -import { LiveShare, RegExpValues } from '../constants'; +import { LiveShare } from '../constants'; // tslint:disable:no-any @@ -24,20 +23,30 @@ export class PostOffice implements IAsyncDisposable { private hostServer : vsls.SharedService | null = null; private guestServer : vsls.SharedServiceProxy | null = null; private currentRole : vsls.Role = vsls.Role.None; + private currentPeerCount: number = 0; + private peerCountChangedEmitter : vscode.EventEmitter = new vscode.EventEmitter(); private commandMap : { [key: string] : { thisArg: any; callback(...args: any[]) : void } } = {}; - constructor(name: string, private liveShareApi: ILiveShareApi) { + constructor( + name: string, + private liveShareApi: ILiveShareApi, + private hostArgsTranslator?: (api: vsls.LiveShare | null, command: string, role: vsls.Role, args: any[]) => void) { this.name = name; this.started = this.startCommandServer(); // Note to self, could the callbacks be keeping things alive that we don't want to be alive? } - public role = () => { - return this.currentRole; + public get peerCount() { + return this.currentPeerCount; + } + + public get peerCountChanged() : vscode.Event { + return this.peerCountChangedEmitter.event; } public async dispose() { + this.peerCountChangedEmitter.dispose(); if (this.hostServer) { const s = await this.started; if (s !== null) { @@ -53,11 +62,6 @@ export class PostOffice implements IAsyncDisposable { const api = await this.started; let skipDefault = false; - // Every command should generate an extra arg - the id. This lets them - // be sync'd between guest and host. - const id = uuid(); - const modifiedArgs = [...args, id]; - if (api && api.session) { switch (this.currentRole) { case vsls.Role.Guest: @@ -70,7 +74,7 @@ export class PostOffice implements IAsyncDisposable { case vsls.Role.Host: // Notify everybody and call our local callback (by falling through) if (this.hostServer) { - this.hostServer.notify(this.escapeCommandName(command), this.translateArgs(api, command, ...modifiedArgs)); + this.hostServer.notify(this.escapeCommandName(command), this.translateArgs(api, command, ...args)); } break; default: @@ -80,7 +84,7 @@ export class PostOffice implements IAsyncDisposable { if (!skipDefault) { // Default when not connected is to just call the registered callback - this.callCallback(command, ...modifiedArgs); + this.callCallback(command, ...args); } } @@ -94,7 +98,6 @@ export class PostOffice implements IAsyncDisposable { // Always stick in the command map so that if we switch roles, we reregister this.commandMap[command] = { callback, thisArg }; - } private createBroadcastArgs(command: string, ...args: any[]) : IMessageArgs { @@ -102,31 +105,6 @@ export class PostOffice implements IAsyncDisposable { } private translateArgs(api: vsls.LiveShare, command: string, ...args: any[]) : IMessageArgs { - // Some file path args need to have their values translated to guest - // uri format for use on a guest. Try to find any file arguments - const callback = this.commandMap.hasOwnProperty(command) ? this.commandMap[command].callback : undefined; - if (callback) { - const str = callback.toString(); - - // Early check - if (str.includes('file')) { - const callbackArgs = str.match(RegExpValues.ParamsExractorRegEx); - if (callbackArgs && callbackArgs.length > 1) { - const argNames = callbackArgs[1].match(RegExpValues.ArgsSplitterRegEx); - if (argNames && argNames.length > 0) { - for (let i = 0; i < args.length; i += 1) { - if (argNames[i].includes('file')) { - const file = args[i]; - if (typeof file === 'string') { - args[i] = api.convertLocalUriToShared(vscode.Uri.file(file)).fsPath; - } - } - } - } - } - } - } - // Make sure to eliminate all .toJSON functions on our arguments. Otherwise they're stringified incorrectly for (let a = 0; a <= args.length; a += 1) { // Eliminate this on only object types (https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript) @@ -135,8 +113,21 @@ export class PostOffice implements IAsyncDisposable { } } + // Copy our args so we don't affect callers. + const copyArgs = JSON.parse(JSON.stringify(args)); + + // Some file path args need to have their values translated to guest + // uri format for use on a guest. Try to find any file arguments + const callback = this.commandMap.hasOwnProperty(command) ? this.commandMap[command].callback : undefined; + if (callback) { + // Give the passed in args translator a chance to attempt a translation + if (this.hostArgsTranslator) { + this.hostArgsTranslator(api, command, vsls.Role.Host, copyArgs); + } + } + // Then wrap them all up in a string. - return { args: JSON.stringify(args) }; + return { args: JSON.stringify(copyArgs) }; } private escapeCommandName(command: string) : string { @@ -179,7 +170,9 @@ export class PostOffice implements IAsyncDisposable { const api = await this.liveShareApi.getApi(); if (api !== null) { api.onDidChangeSession(() => this.onChangeSession(api).ignoreErrors()); + api.onDidChangePeers(() => this.onChangePeers(api).ignoreErrors()); await this.onChangeSession(api); + await this.onChangePeers(api); } return api; } @@ -205,7 +198,7 @@ export class PostOffice implements IAsyncDisposable { // When we start the host, listen for the broadcast message if (this.hostServer !== null) { - this.hostServer.onNotify(LiveShare.LiveShareBroadcastRequest, a => this.onBroadcastRequest(a as IMessageArgs)); + this.hostServer.onNotify(LiveShare.LiveShareBroadcastRequest, a => this.onBroadcastRequest(api, a as IMessageArgs)); } } else if (api.session.role === vsls.Role.Guest) { this.guestServer = await api.getSharedService(this.name); @@ -216,15 +209,33 @@ export class PostOffice implements IAsyncDisposable { } } - private onBroadcastRequest = (a: IMessageArgs) => { + private async onChangePeers(api: vsls.LiveShare) : Promise { + let newPeerCount = 0; + if (api.session) { + newPeerCount = api.peers.length; + } + if (newPeerCount !== this.currentPeerCount) { + this.peerCountChangedEmitter.fire(newPeerCount); + this.currentPeerCount = newPeerCount; + } + } + + private onBroadcastRequest = (api: vsls.LiveShare, a: IMessageArgs) => { // This means we need to rebroadcast a request. We should also handle this request ourselves (as this means // a guest is trying to tell everybody about a command) if (a.args.length > 0) { const jsonArray = JSON.parse(a.args) as JSONArray; if (jsonArray !== null && jsonArray.length >= 2) { const firstArg = jsonArray[0]; // More stupid hygiene problems. - const command = firstArg !== null ? firstArg!.toString() : ''; - this.postCommand(command, ...jsonArray.slice(1)).ignoreErrors(); + const command = firstArg !== null ? firstArg.toString() : ''; + + // Args need to be translated from guest to host + const rest = jsonArray.slice(1); + if (this.hostArgsTranslator) { + this.hostArgsTranslator(api, command, vsls.Role.Guest, rest); + } + + this.postCommand(command, ...rest).ignoreErrors(); } } } diff --git a/src/client/datascience/serviceRegistry.ts b/src/client/datascience/serviceRegistry.ts index f2d67bf5e5d0..8ba7fe063de5 100644 --- a/src/client/datascience/serviceRegistry.ts +++ b/src/client/datascience/serviceRegistry.ts @@ -3,7 +3,6 @@ 'use strict'; import { IServiceManager } from '../ioc/types'; import { CodeCssGenerator } from './codeCssGenerator'; -import { CommandBroker } from './commandBroker'; import { DataScience } from './datascience'; import { DataScienceCodeLensProvider } from './editor-integration/codelensprovider'; import { CodeWatcher } from './editor-integration/codewatcher'; @@ -15,14 +14,13 @@ import { JupyterExecutionFactory } from './jupyter/jupyterExecutionFactory'; import { JupyterExporter } from './jupyter/jupyterExporter'; import { JupyterImporter } from './jupyter/jupyterImporter'; import { JupyterServerFactory } from './jupyter/jupyterServerFactory'; -import { JupyterServerManager } from './jupyter/jupyterServerManager'; import { JupyterSessionManager } from './jupyter/jupyterSessionManager'; import { JupyterVariables } from './jupyter/jupyterVariables'; import { StatusProvider } from './statusProvider'; +import { ThemeFinder } from './themeFinder'; import { ICodeCssGenerator, ICodeWatcher, - ICommandBroker, IDataScience, IDataScienceCodeLensProvider, IDataScienceCommandListener, @@ -35,8 +33,8 @@ import { INotebookExporter, INotebookImporter, INotebookServer, - INotebookServerManager, - IStatusProvider + IStatusProvider, + IThemeFinder } from './types'; export function registerTypes(serviceManager: IServiceManager) { @@ -44,17 +42,16 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IDataScience, DataScience); serviceManager.addSingleton(IJupyterExecution, JupyterExecutionFactory); serviceManager.add(IDataScienceCommandListener, HistoryCommandListener); - serviceManager.addSingleton(ICommandBroker, CommandBroker); serviceManager.addSingleton(IHistoryProvider, HistoryProvider); serviceManager.add(IHistory, History); serviceManager.add(INotebookExporter, JupyterExporter); serviceManager.add(INotebookImporter, JupyterImporter); - serviceManager.addSingleton(INotebookServerManager, JupyterServerManager); - serviceManager.addSingleton(INotebookServer, JupyterServerFactory); + serviceManager.add(INotebookServer, JupyterServerFactory); serviceManager.addSingleton(ICodeCssGenerator, CodeCssGenerator); serviceManager.addSingleton(IStatusProvider, StatusProvider); serviceManager.addSingleton(IJupyterSessionManager, JupyterSessionManager); serviceManager.addSingleton(IJupyterVariables, JupyterVariables); serviceManager.add(ICodeWatcher, CodeWatcher); serviceManager.add(IJupyterCommandFactory, JupyterCommandFactory); + serviceManager.addSingleton(IThemeFinder, ThemeFinder); } diff --git a/src/client/datascience/statusProvider.ts b/src/client/datascience/statusProvider.ts index e69e9c2c2fb7..3add9fb6a32b 100644 --- a/src/client/datascience/statusProvider.ts +++ b/src/client/datascience/statusProvider.ts @@ -6,7 +6,7 @@ import { Disposable, ProgressLocation, ProgressOptions } from 'vscode'; import { IApplicationShell } from '../common/application/types'; import { createDeferred, Deferred } from '../common/utils/async'; -import { HistoryMessages } from './constants'; +import { HistoryMessages } from './historyTypes'; import { IHistoryProvider, IStatusProvider } from './types'; class StatusItem implements Disposable { diff --git a/src/client/datascience/themeFinder.ts b/src/client/datascience/themeFinder.ts new file mode 100644 index 000000000000..757ef3f1220c --- /dev/null +++ b/src/client/datascience/themeFinder.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as fs from 'fs-extra'; +import * as glob from 'glob'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; + +import { ICurrentProcess, IExtensions, ILogger } from '../common/types'; + +// tslint:disable:no-any + +interface IThemeData { + rootFile: string; + isDark : boolean; +} + +@injectable() +export class ThemeFinder { + private themeCache : { [key: string] : IThemeData | undefined } = {}; + + constructor( + @inject(IExtensions) private extensions: IExtensions, + @inject(ICurrentProcess) private currentProcess: ICurrentProcess, + @inject(ILogger) private logger: ILogger) { + } + + public async findThemeRootJson(themeName: string) : Promise { + // find our data + const themeData = await this.findThemeData(themeName); + + // Use that data if it worked + if (themeData) { + return themeData.rootFile; + } + } + + public async isThemeDark(themeName: string) : Promise { + // find our data + const themeData = await this.findThemeData(themeName); + + // Use that data if it worked + if (themeData) { + return themeData.isDark; + } + } + + private async findThemeData(themeName: string) : Promise { + // See if already found it or not + if (!this.themeCache.hasOwnProperty(themeName)) { + try { + this.themeCache[themeName] = await this.findMatchingTheme(themeName); + } catch (exc) { + this.logger.logError(exc); + } + } + return this.themeCache[themeName]; + } + + private async findMatchingTheme(themeName: string) : Promise { + // Look through all extensions to find the theme. This will search + // the default extensions folder and our installed extensions. + const extensions = this.extensions.all; + for (const e of extensions) { + const result = await this.findMatchingThemeFromJson(path.join(e.extensionPath, 'package.json'), themeName); + if (result) { + return result; + } + } + + // If didn't find in the extensions folder, then try searching manually. This shouldn't happen, but + // this is our backup plan in case vscode changes stuff. + const currentExe = this.currentProcess.execPath; + let currentPath = path.dirname(currentExe); + + // Should be somewhere under currentPath/resources/app/extensions inside of a json file + let extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions'); + if (!(await fs.pathExists(extensionsPath))) { + // Might be on mac or linux. try a different path + currentPath = path.resolve(currentPath, '../../../..'); + extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions'); + } + const other = await this.findMatchingThemes(extensionsPath, themeName); + if (other) { + return other; + } + } + + private async findMatchingThemes(rootPath: string, themeName: string) : Promise { + // Search through all package.json files in the directory and below, looking + // for the themeName in them. + const foundPackages = await new Promise((resolve, reject) => { + glob('**/package.json', { cwd: rootPath }, (err, matches) => { + if (err) { + reject(err); + } + resolve(matches); + }); + }); + if (foundPackages.length > 0) { + // For each one, open it up and look for the theme name. + for (const f of foundPackages) { + const fpath = path.join(rootPath, f); + const data = await this.findMatchingThemeFromJson(fpath, themeName); + if (data) { + return data; + } + } + } + } + + private async findMatchingThemeFromJson(packageJson: string, themeName: string) : Promise { + // Read the contents of the json file + const json = await fs.readJSON(packageJson, { encoding: 'utf-8'}); + + // Should have a name entry and a contributes entry + if (json.hasOwnProperty('name') && json.hasOwnProperty('contributes')) { + // See if contributes has a theme + const contributes = json['contributes']; + if (contributes.hasOwnProperty('themes')) { + const themes = contributes['themes'] as any[]; + // Go through each theme, seeing if the label matches our theme name + for (const t of themes) { + if ((t.hasOwnProperty('label') && t['label'] === themeName) || + (t.hasOwnProperty('id') && t['id'] === themeName)) { + const isDark = t.hasOwnProperty('uiTheme') && t['uiTheme'] === 'vs-dark'; + // Path is relative to the package.json file. + const rootFile = t.hasOwnProperty('path') ? path.join(path.dirname(packageJson), t['path'].toString()) : ''; + + return {isDark, rootFile}; + } + } + } + } + } +} diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 7c71513a9035..f317482ae219 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -21,7 +21,7 @@ export interface IDataScience extends Disposable { export const IDataScienceCommandListener = Symbol('IDataScienceCommandListener'); export interface IDataScienceCommandListener { - register(commandManager: ICommandBroker): void; + register(commandManager: ICommandManager): void; } // Connection information for talking to a jupyter notebook process @@ -46,14 +46,7 @@ export interface INotebookServerLaunchInfo kernelSpec: IJupyterKernelSpec | undefined; usingDarkTheme: boolean; workingDir: string | undefined; -} - -// Manage our running notebook server instances -export const INotebookServerManager = Symbol('INotebookServerManager'); -export interface INotebookServerManager { - getOrCreateServer(): Promise; - getServer() : Promise; - getActiveServer(): INotebookServer | undefined; + purpose: string | undefined; // Purpose this server is for } // Talks to a jupyter ipython kernel to retrieve data for cells @@ -67,21 +60,31 @@ export interface INotebookServer extends IAsyncDisposable { shutdown() : Promise; interruptKernel(timeoutInMs: number) : Promise; setInitialDirectory(directory: string): Promise; - getLaunchInfo(): INotebookServerLaunchInfo | undefined; + waitForConnect(): Promise; getConnectionInfo(): IConnection | undefined; getSysInfo() : Promise; } +export interface INotebookServerOptions { + uri?: string; + usingDarkTheme?: boolean; + useDefaultConfig?: boolean; + workingDir?: string; + purpose: string; +} + export const IJupyterExecution = Symbol('IJupyterExecution'); export interface IJupyterExecution extends IAsyncDisposable { isNotebookSupported(cancelToken?: CancellationToken) : Promise; isImportSupported(cancelToken?: CancellationToken) : Promise; isKernelCreateSupported(cancelToken?: CancellationToken): Promise; isKernelSpecSupported(cancelToken?: CancellationToken): Promise; - connectToNotebookServer(uri: string | undefined, usingDarkTheme: boolean, useDefaultConfig: boolean, cancelToken?: CancellationToken, workingDir?: string) : Promise; + isSpawnSupported(cancelToken?: CancellationToken): Promise; + connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken) : Promise; spawnNotebook(file: string) : Promise; importNotebook(file: string, template: string) : Promise; getUsableJupyterPython(cancelToken?: CancellationToken) : Promise; + getServer(options?: INotebookServerOptions) : Promise; } export const IJupyterSession = Symbol('IJupyterSession'); @@ -118,14 +121,15 @@ export const IHistoryProvider = Symbol('IHistoryProvider'); export interface IHistoryProvider { getActive() : IHistory | undefined; - getOrCreateActive(): IHistory; + getOrCreateActive(): Promise; + getNotebookOptions() : Promise; } export const IHistory = Symbol('IHistory'); export interface IHistory extends Disposable { closed: Event; show() : Promise; - addCode(code: string, file: string, line: number, id: string, editor?: TextEditor) : Promise; + addCode(code: string, file: string, line: number, editor?: TextEditor) : Promise; // tslint:disable-next-line:no-any postMessage(type: string, payload?: any): void; undoCells(): void; @@ -161,11 +165,11 @@ export interface ICodeWatcher { getVersion() : number; getCodeLenses() : CodeLens[]; getCachedSettings() : IDataScienceSettings | undefined; - runAllCells(id: string): void; - runCell(range: Range, id: string): void; - runCurrentCell(id: string): void; - runCurrentCellAndAdvance(id: string): void; - runSelectionOrLine(activeEditor: TextEditor | undefined, id: string): void; + runAllCells(): void; + runCell(range: Range): void; + runCurrentCell(): void; + runCurrentCellAndAdvance(): void; + runSelectionOrLine(activeEditor: TextEditor | undefined): void; } export enum CellState { @@ -205,6 +209,12 @@ export interface ICodeCssGenerator { generateThemeCss() : Promise; } +export const IThemeFinder = Symbol('IThemeFinder'); +export interface IThemeFinder { + findThemeRootJson(themeName: string) : Promise; + isThemeDark(themeName: string) : Promise; +} + export const IStatusProvider = Symbol('IStatusProvider'); export interface IStatusProvider { // call this function to set the new status on the active @@ -234,11 +244,6 @@ export interface IDataScienceExtraSettings extends IDataScienceSettings { }; } -export const ICommandBroker = Symbol('ICommandBroker'); - -export interface ICommandBroker extends ICommandManager { -} - // Get variables from the currently running active Jupyter server export interface IJupyterVariable { name: string; diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 09995421cadb..26a2be1349f2 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -315,6 +315,7 @@ interface IEventNamePropertyMapping { [Telemetry.ImportNotebook]: { scope: 'command' | 'file' }; [Telemetry.Interrupt]: never | undefined; [Telemetry.Redo]: never | undefined; + [Telemetry.RemoteAddCode]: never | undefined; [Telemetry.RestartKernel]: never | undefined; [Telemetry.RunAllCells]: never | undefined; [Telemetry.RunSelectionOrLine]: never | undefined; diff --git a/src/datascience-ui/history-react/MainPanel.tsx b/src/datascience-ui/history-react/MainPanel.tsx index 868f73419b4d..92a3c3e3801f 100644 --- a/src/datascience-ui/history-react/MainPanel.tsx +++ b/src/datascience-ui/history-react/MainPanel.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import { CellMatcher } from '../../client/datascience/cellMatcher'; import { generateMarkdownFromCodeLines } from '../../client/datascience/common'; -import { HistoryMessages, HistoryNonLiveShareMessages } from '../../client/datascience/constants'; +import { HistoryMessages } from '../../client/datascience/historyTypes'; import { CellState, ICell, IHistoryInfo } from '../../client/datascience/types'; import { ErrorBoundary } from '../react-common/errorBoundary'; import { getLocString } from '../react-common/locReactSide'; @@ -78,7 +78,7 @@ export class MainPanel extends React.Component // If haven't sent our startup message, send it now. if (!this.sentStartup) { this.sentStartup = true; - PostOffice.sendMessage({type: HistoryNonLiveShareMessages.Started}); + PostOffice.sendMessage(HistoryMessages.Started); } const progressBar = this.state.busy && !this.props.testMode ? : undefined; @@ -208,7 +208,7 @@ export class MainPanel extends React.Component return cellVM.cell; }); - PostOffice.sendMessage({type: HistoryMessages.ReturnAllCells, payload: { contents: cells }}); + PostOffice.sendMessage(HistoryMessages.ReturnAllCells, cells); } private renderExtraButtons = () => { @@ -296,11 +296,11 @@ export class MainPanel extends React.Component const cellVM = this.state.cellVMs[index]; // Send a message to the other side to jump to a particular cell - PostOffice.sendMessage({ type: HistoryMessages.GotoCodeCell, payload: { file : cellVM.cell.file, line: cellVM.cell.line }}); + PostOffice.sendMessage(HistoryMessages.GotoCodeCell, { file : cellVM.cell.file, line: cellVM.cell.line }); } private deleteCell = (index: number) => { - PostOffice.sendMessage({ type: HistoryMessages.DeleteCell, payload: { }}); + PostOffice.sendMessage(HistoryMessages.DeleteCell); // Update our state this.setState({ @@ -313,17 +313,17 @@ export class MainPanel extends React.Component } private collapseAll = () => { - PostOffice.sendMessage({ type: HistoryMessages.CollapseAll, payload: { }}); + PostOffice.sendMessage(HistoryMessages.CollapseAll); this.collapseAllSilent(); } private expandAll = () => { - PostOffice.sendMessage({ type: HistoryMessages.ExpandAll, payload: { }}); + PostOffice.sendMessage(HistoryMessages.ExpandAll); this.expandAllSilent(); } private clearAll = () => { - PostOffice.sendMessage({ type: HistoryMessages.DeleteAllCells, payload: { }}); + PostOffice.sendMessage(HistoryMessages.DeleteAllCells); this.clearAllSilent(); } @@ -348,7 +348,7 @@ export class MainPanel extends React.Component const cells = this.state.redoStack[this.state.redoStack.length - 1]; const redoStack = this.state.redoStack.slice(0, this.state.redoStack.length - 1); const undoStack = this.pushStack(this.state.undoStack, this.state.cellVMs); - PostOffice.sendMessage({ type: HistoryMessages.Redo, payload: { }}); + PostOffice.sendMessage(HistoryMessages.Redo); this.setState({ cellVMs: cells, undoStack: undoStack, @@ -365,7 +365,7 @@ export class MainPanel extends React.Component const cells = this.state.undoStack[this.state.undoStack.length - 1]; const undoStack = this.state.undoStack.slice(0, this.state.undoStack.length - 1); const redoStack = this.pushStack(this.state.redoStack, this.state.cellVMs); - PostOffice.sendMessage({ type: HistoryMessages.Undo, payload: { }}); + PostOffice.sendMessage(HistoryMessages.Undo); this.setState({ cellVMs: cells, undoStack : undoStack, @@ -379,18 +379,18 @@ export class MainPanel extends React.Component private restartKernel = () => { // Send a message to the other side to restart the kernel - PostOffice.sendMessage({ type: HistoryMessages.RestartKernel, payload: { }}); + PostOffice.sendMessage(HistoryMessages.RestartKernel); } private interruptKernel = () => { // Send a message to the other side to restart the kernel - PostOffice.sendMessage({ type: HistoryMessages.Interrupt, payload: { }}); + PostOffice.sendMessage(HistoryMessages.Interrupt); } private export = () => { // Send a message to the other side to export our current list const cellContents: ICell[] = this.state.cellVMs.map((cellVM: ICellViewModel, index: number) => { return cellVM.cell; }); - PostOffice.sendMessage({ type: HistoryMessages.Export, payload: { contents: cellContents }}); + PostOffice.sendMessage(HistoryMessages.Export, cellContents); } private scrollToBottom = () => { @@ -561,7 +561,7 @@ export class MainPanel extends React.Component undoCount: this.state.undoStack.length, redoCount: this.state.redoStack.length }; - PostOffice.sendMessage({type: HistoryNonLiveShareMessages.SendInfo, payload: { info: info }}); + PostOffice.sendMessage(HistoryMessages.SendInfo, info); } private updateOrAdd = (cell: ICell, allowAdd? : boolean) => { @@ -668,7 +668,7 @@ export class MainPanel extends React.Component // Send a message to execute this code if necessary. if (editCell.cell.state !== CellState.finished) { - PostOffice.sendMessage({ type: HistoryMessages.SubmitNewCell, payload: { code: code, id: editCell.cell.id }}); + PostOffice.sendMessage(HistoryMessages.SubmitNewCell, { code, id: editCell.cell.id }); } } } diff --git a/src/datascience-ui/history-react/cellButton.css b/src/datascience-ui/history-react/cellButton.css index 64584d8bfdf2..347a0b6437ca 100644 --- a/src/datascience-ui/history-react/cellButton.css +++ b/src/datascience-ui/history-react/cellButton.css @@ -32,6 +32,10 @@ max-height: 100%; } +.cell-button-image svg{ + pointer-events: none; +} + .cell-button-vscode-light:disabled { border-color: gray; filter: grayscale(100%); diff --git a/src/datascience-ui/react-common/postOffice.tsx b/src/datascience-ui/react-common/postOffice.tsx index 883709ee6e60..f8ac5eb30f1b 100644 --- a/src/datascience-ui/react-common/postOffice.tsx +++ b/src/datascience-ui/react-common/postOffice.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { WebPanelMessage } from '../../client/common/application/types'; +import { IHistoryMapping } from '../../client/datascience/historyTypes'; export interface IVsCodeApi { // tslint:disable-next-line:no-any @@ -43,11 +44,11 @@ export class PostOffice extends React.Component { return false; } - public static sendMessage(message: WebPanelMessage) { + public static sendMessage(type: T, payload?: M[T]) { if (PostOffice.canSendMessages()) { const api = PostOffice.acquireApi(); if (api) { - api.postMessage(message); + api.postMessage({type: type.toString(), payload }); } } } diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 5aca3abb09fe..43b0d5c41fd4 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -59,6 +59,7 @@ import { IAsyncDisposableRegistry, IConfigurationService, ICurrentProcess, + IExtensions, ILogger, IPathUtils, IPersistentStateFactory, @@ -76,9 +77,9 @@ import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyte import { JupyterExporter } from '../../client/datascience/jupyter/jupyterExporter'; import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter'; import { JupyterServerFactory } from '../../client/datascience/jupyter/jupyterServerFactory'; -import { JupyterServerManager } from '../../client/datascience/jupyter/jupyterServerManager'; import { JupyterSessionManager } from '../../client/datascience/jupyter/jupyterSessionManager'; import { StatusProvider } from '../../client/datascience/statusProvider'; +import { ThemeFinder } from '../../client/datascience/themeFinder'; import { ICodeCssGenerator, IDataScience, @@ -90,8 +91,8 @@ import { INotebookExporter, INotebookImporter, INotebookServer, - INotebookServerManager, - IStatusProvider + IStatusProvider, + IThemeFinder } from '../../client/datascience/types'; import { EnvironmentActivationService } from '../../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; @@ -162,6 +163,7 @@ import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs import { MockAutoSelectionService } from '../mocks/autoSelector'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { MockCommandManager } from './mockCommandManager'; +import { MockExtensions } from './mockExtensions'; import { MockJupyterManager } from './mockJupyterManager'; import { MockLiveShareApi } from './mockLiveShare'; @@ -200,14 +202,15 @@ export class DataScienceIocContainer extends UnitTestIocContainer { public registerDataScienceTypes() { this.registerFileSystemTypes(); this.serviceManager.addSingleton(IJupyterExecution, JupyterExecutionFactory); - this.serviceManager.addSingleton(INotebookServerManager, JupyterServerManager); this.serviceManager.addSingleton(IHistoryProvider, HistoryProvider); this.serviceManager.add(IHistory, History); this.serviceManager.add(INotebookImporter, JupyterImporter); this.serviceManager.add(INotebookExporter, JupyterExporter); this.serviceManager.addSingleton(ILiveShareApi, MockLiveShareApi); + this.serviceManager.addSingleton(IExtensions, MockExtensions); this.serviceManager.add(INotebookServer, JupyterServerFactory); this.serviceManager.add(IJupyterCommandFactory, JupyterCommandFactory); + this.serviceManager.addSingleton(IThemeFinder, ThemeFinder); this.serviceManager.addSingleton(ICodeCssGenerator, CodeCssGenerator); this.serviceManager.addSingleton(IStatusProvider, StatusProvider); this.serviceManager.add(IKnownSearchPathsForInterpreters, KnownSearchPathsForInterpreters); diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 62e58cde5d1f..51b028a4b5da 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -5,7 +5,6 @@ // Disable whitespace / multiline as we use that to pass in our fake file strings import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; -import * as uuid from 'uuid/v4'; import { CancellationTokenSource, CodeLens, Range, Selection, TextEditor } from 'vscode'; import { IApplicationShell, IDocumentManager } from '../../../client/common/application/types'; @@ -20,6 +19,8 @@ import { IServiceContainer } from '../../../client/ioc/types'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; import { createDocument } from './helpers'; +//tslint:disable:no-any + suite('DataScience Code Watcher Unit Tests', () => { let codeWatcher: CodeWatcher; let appShell: TypeMoq.IMock; @@ -43,7 +44,7 @@ suite('DataScience Code Watcher Unit Tests', () => { appShell = TypeMoq.Mock.ofType(); logger = TypeMoq.Mock.ofType(); historyProvider = TypeMoq.Mock.ofType(); - activeHistory = TypeMoq.Mock.ofType(); + activeHistory = createTypeMoq('history'); documentManager = TypeMoq.Mock.ofType(); textEditor = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); @@ -74,7 +75,7 @@ suite('DataScience Code Watcher Unit Tests', () => { serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICodeWatcher))).returns(() => new CodeWatcher(appShell.object, logger.object, historyProvider.object, fileSystem.object, configService.object, documentManager.object)); // Setup our active history instance - historyProvider.setup(h => h.getOrCreateActive()).returns(() => activeHistory.object); + historyProvider.setup(h => h.getOrCreateActive()).returns(() => Promise.resolve(activeHistory.object)); // Setup our active text editor documentManager.setup(dm => dm.activeTextEditor).returns(() => textEditor.object); @@ -88,6 +89,15 @@ suite('DataScience Code Watcher Unit Tests', () => { codeWatcher = new CodeWatcher(appShell.object, logger.object, historyProvider.object, fileSystem.object, configService.object, documentManager.object); }); + function createTypeMoq(tag: string): TypeMoq.IMock { + // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class + // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 + const result: TypeMoq.IMock = TypeMoq.Mock.ofType(); + (result as any)['tag'] = tag; + result.setup((x: any) => x.then).returns(() => undefined); + return result; + } + test('Add a file with just a #%% mark to a code watcher', () => { const fileName = 'test.py'; const version = 1; @@ -307,13 +317,12 @@ fourth line activeHistory.setup(h => h.addCode(TypeMoq.It.isValue(testString), TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), TypeMoq.It.is((ed: TextEditor) => { return textEditor.object === ed; }))).verifiable(TypeMoq.Times.once()); // Try our RunCell command - await codeWatcher.runCell(testRange, uuid()); + await codeWatcher.runCell(testRange); // Verify function calls activeHistory.verifyAll(); @@ -354,7 +363,7 @@ testing2`; // Command tests override getText, so just need the ranges here )).verifiable(TypeMoq.Times.once()); // Try our RunCell command - await codeWatcher.runAllCells(uuid()); + await codeWatcher.runAllCells(); // Verify function calls activeHistory.verifyAll(); @@ -378,7 +387,6 @@ testing2`; activeHistory.setup(h => h.addCode(TypeMoq.It.isValue('testing2'), TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), TypeMoq.It.is((ed: TextEditor) => { return textEditor.object === ed; }))).verifiable(TypeMoq.Times.once()); @@ -387,7 +395,7 @@ testing2`; textEditor.setup(te => te.selection).returns(() => new Selection(2, 0, 2, 0)); // Try our RunCell command with the first selection point - await codeWatcher.runCurrentCell(uuid()); + await codeWatcher.runCurrentCell(); // Verify function calls activeHistory.verifyAll(); @@ -410,7 +418,6 @@ testing2`; activeHistory.setup(h => h.addCode(TypeMoq.It.isValue('testing2'), TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(3), - TypeMoq.It.isAny(), TypeMoq.It.is((ed: TextEditor) => { return textEditor.object === ed; }))).verifiable(TypeMoq.Times.once()); @@ -420,7 +427,7 @@ testing2`; textEditor.setup(te => te.selection).returns(() => new Selection(3, 0, 3, 0)); // Try our RunCell command with the first selection point - await codeWatcher.runSelectionOrLine(textEditor.object, uuid()); + await codeWatcher.runSelectionOrLine(textEditor.object); // Verify function calls activeHistory.verifyAll(); @@ -440,7 +447,6 @@ testing2`; // We don't want to ever call add code here activeHistory.setup(h => h.addCode(TypeMoq.It.isAny(), - TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).verifiable(TypeMoq.Times.never()); @@ -449,7 +455,7 @@ testing2`; textEditor.setup(te => te.selection).returns(() => new Selection(0, 0, 0, 0)); // Try our RunCell command with the first selection point - await codeWatcher.runCurrentCell(uuid()); + await codeWatcher.runCurrentCell(); // Verify function calls activeHistory.verifyAll(); @@ -475,7 +481,6 @@ testing2`; // Command tests override getText, so just need the ranges here activeHistory.setup(h => h.addCode(TypeMoq.It.isValue(testString), TypeMoq.It.isValue('test.py'), TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), TypeMoq.It.is((ed: TextEditor) => { return textEditor.object === ed; }))).verifiable(TypeMoq.Times.once()); @@ -501,7 +506,7 @@ testing2`; // Command tests override getText, so just need the ranges here expect(targetRange.end.character).is.equal(8, 'Incorrect range in run cell and advance'); }; - await codeWatcher.runCurrentCellAndAdvance(uuid()); + await codeWatcher.runCurrentCellAndAdvance(); // Verify function calls textEditor.verifyAll(); diff --git a/src/test/datascience/execution.unit.test.ts b/src/test/datascience/execution.unit.test.ts index 0d6410146f2d..805a63c51daa 100644 --- a/src/test/datascience/execution.unit.test.ts +++ b/src/test/datascience/execution.unit.test.ts @@ -90,7 +90,7 @@ class MockJupyterServer implements INotebookServer { public getConnectionInfo(): IConnection | undefined { return this.launchInfo ? this.launchInfo.connectionInfo : undefined; } - public getLaunchInfo(): INotebookServerLaunchInfo | undefined { + public waitForConnect(): Promise { throw new Error('Method not implemented'); } public async shutdown() { @@ -584,24 +584,24 @@ suite('Jupyter Execution', async () => { await assert.eventually.equal(execution.isKernelCreateSupported(), true, 'Kernel Create not supported'); const usableInterpreter = await execution.getUsableJupyterPython(); assert.isOk(usableInterpreter, 'Usable intepreter not found'); - await assert.isFulfilled(execution.connectToNotebookServer(undefined, false, true), 'Should be able to start a server'); + await assert.isFulfilled(execution.connectToNotebookServer(), 'Should be able to start a server'); }).timeout(10000); test('Failing notebook throws exception', async () => { const execution = createExecution(missingNotebookPython); when(interpreterService.getInterpreters()).thenResolve([missingNotebookPython]); - await assert.isRejected(execution.connectToNotebookServer(undefined, false, true), 'Running cells requires Jupyter notebooks to be installed.'); + await assert.isRejected(execution.connectToNotebookServer(), 'Running cells requires Jupyter notebooks to be installed.'); }).timeout(10000); test('Failing others throws exception', async () => { const execution = createExecution(missingNotebookPython); when(interpreterService.getInterpreters()).thenResolve([missingNotebookPython, missingNotebookPython2]); - await assert.isRejected(execution.connectToNotebookServer(undefined, false, true), 'Running cells requires Jupyter notebooks to be installed.'); + await assert.isRejected(execution.connectToNotebookServer(), 'Running cells requires Jupyter notebooks to be installed.'); }).timeout(10000); test('Slow notebook startups throws exception', async () => { const execution = createExecution(workingPython, ['Failure']); - await assert.isRejected(execution.connectToNotebookServer(undefined, false, true), 'Jupyter notebook failed to launch. \r\nError: The Jupyter notebook server failed to launch in time\nFailure'); + await assert.isRejected(execution.connectToNotebookServer(), 'Jupyter notebook failed to launch. \r\nError: The Jupyter notebook server failed to launch in time\nFailure'); }).timeout(10000); test('Other than active works', async () => { @@ -653,7 +653,7 @@ suite('Jupyter Execution', async () => { test('Kernelspec is deleted on exit', async () => { const execution = createExecution(missingKernelPython); - await assert.isFulfilled(execution.connectToNotebookServer(undefined, false, true), 'Should be able to start a server'); + await assert.isFulfilled(execution.connectToNotebookServer(), 'Should be able to start a server'); await cleanupDisposables(); const exists = fs.existsSync(workingKernelSpec); assert.notOk(exists, 'Temp kernel spec still exists'); @@ -665,7 +665,7 @@ suite('Jupyter Execution', async () => { const execution = createExecution(missingNotebookPython); when(interpreterService.getInterpreters()).thenResolve([missingNotebookPython]); when(fileSystem.getFiles(anyString())).thenResolve([jupyterOnPath]); - await assert.isFulfilled(execution.connectToNotebookServer(undefined, false, true), 'Should be able to start a server'); + await assert.isFulfilled(execution.connectToNotebookServer(), 'Should be able to start a server'); }).timeout(10000); test('Jupyter found on the path skipped', async () => { @@ -674,6 +674,6 @@ suite('Jupyter Execution', async () => { const execution = createExecution(missingNotebookPython, undefined, true); when(interpreterService.getInterpreters()).thenResolve([missingNotebookPython]); when(fileSystem.getFiles(anyString())).thenResolve([jupyterOnPath]); - await assert.isRejected(execution.connectToNotebookServer(undefined, false, true), 'Running cells requires Jupyter notebooks to be installed.'); + await assert.isRejected(execution.connectToNotebookServer(), 'Running cells requires Jupyter notebooks to be installed.'); }).timeout(10000); }); diff --git a/src/test/datascience/history.functional.test.tsx b/src/test/datascience/history.functional.test.tsx index f46b3fa81a31..fb2934c96c4c 100644 --- a/src/test/datascience/history.functional.test.tsx +++ b/src/test/datascience/history.functional.test.tsx @@ -9,7 +9,6 @@ import * as path from 'path'; import * as React from 'react'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import * as uuid from 'uuid/v4'; import { CancellationToken, Disposable, TextDocument, TextEditor } from 'vscode'; import { @@ -25,8 +24,9 @@ import { IDataScienceSettings } from '../../client/common/types'; import { createDeferred, Deferred } from '../../client/common/utils/async'; import { noop } from '../../client/common/utils/misc'; import { Architecture } from '../../client/common/utils/platform'; -import { EditorContexts, HistoryMessages, HistoryNonLiveShareMessages } from '../../client/datascience/constants'; +import { EditorContexts } from '../../client/datascience/constants'; import { HistoryMessageListener } from '../../client/datascience/historyMessageListener'; +import { HistoryMessages } from '../../client/datascience/historyTypes'; import { IHistory, IHistoryProvider, IJupyterExecution } from '../../client/datascience/types'; import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { CellButton } from '../../datascience-ui/history-react/cellButton'; @@ -142,13 +142,13 @@ suite('History output tests', () => { delete (global as any)['ascquireVsCodeApi']; }); - function getOrCreateHistory() : IHistory { - const result = historyProvider.getOrCreateActive(); + async function getOrCreateHistory() : Promise { + const result = await historyProvider.getOrCreateActive(); // During testing the MainPanel sends the init message before our history is created. // Pretend like it's happening now const listener = ((result as any)['messageListener']) as HistoryMessageListener; - listener.onMessage(HistoryNonLiveShareMessages.Started, {}); + listener.onMessage(HistoryMessages.Started, {}); return result; } @@ -309,8 +309,8 @@ suite('History output tests', () => { // 4) Output message (if there's only one) // 5) Status finished return getCellResults(wrapper, expectedRenderCount, async () => { - const history = getOrCreateHistory(); - await history.addCode(code, 'foo.py', 2, uuid()); + const history = await getOrCreateHistory(); + await history.addCode(code, 'foo.py', 2); }); } @@ -582,7 +582,7 @@ for _ in range(50): }); runMountedTest('Undo/redo commands', async (wrapper) => { - const history = getOrCreateHistory(); + const history = await getOrCreateHistory(); // Get a cell into the list await addCode(wrapper, 'a=1\na'); @@ -726,7 +726,7 @@ for _ in range(50): // Make sure to create the history after the rebind or it gets the wrong application shell. await addCode(wrapper, 'a=1\na'); - const history = getOrCreateHistory(); + const history = await getOrCreateHistory(); // Export should cause exportCalled to change to true await waitForMessageResponse(() => history.exportCells()); @@ -755,11 +755,11 @@ for _ in range(50): test('Dispose test', async () => { // tslint:disable-next-line:no-any if (await jupyterExecution.isNotebookSupported()) { - const history = getOrCreateHistory(); + const history = await getOrCreateHistory(); await history.show(); // Have to wait for the load to finish await history.dispose(); // tslint:disable-next-line:no-any - const h2 = getOrCreateHistory(); + const h2 = await getOrCreateHistory(); // Check equal and then dispose so the test goes away const equal = Object.is(history, h2); await h2.show(); @@ -772,7 +772,7 @@ for _ in range(50): runMountedTest('Editor Context', async (wrapper) => { // Verify we can send different commands to the UI and it will respond - const history = getOrCreateHistory(); + const history = await getOrCreateHistory(); // Before we have any cells, verify our contexts are not set assert.equal(ioc.getContext(EditorContexts.HaveInteractive), false, 'Should not have interactive before starting'); @@ -783,7 +783,7 @@ for _ in range(50): const updatePromise = waitForUpdate(wrapper, MainPanel); // Send some code to the history - await history.addCode('a=1\na', 'foo.py', 2, uuid()); + await history.addCode('a=1\na', 'foo.py', 2); // Wait for the render to go through await updatePromise; @@ -834,7 +834,7 @@ for _ in range(50): runMountedTest('Simple input', async (wrapper) => { // Create a history so that it listens to the results. - const history = getOrCreateHistory(); + const history = await getOrCreateHistory(); await history.show(); // Then enter some code. @@ -844,7 +844,7 @@ for _ in range(50): runMountedTest('Multiple input', async (wrapper) => { // Create a history so that it listens to the results. - const history = getOrCreateHistory(); + const history = await getOrCreateHistory(); await history.show(); // Then enter some code. diff --git a/src/test/datascience/historyCommandListener.unit.test.ts b/src/test/datascience/historyCommandListener.unit.test.ts index 15411362289d..c14c0b7993e3 100644 --- a/src/test/datascience/historyCommandListener.unit.test.ts +++ b/src/test/datascience/historyCommandListener.unit.test.ts @@ -305,7 +305,7 @@ suite('History command listener', async () => { createCommandListener(undefined); const doc = await documentManager.openTextDocument('bar.ipynb'); await documentManager.showTextDocument(doc); - when(jupyterExecution.connectToNotebookServer(anything(), anything(), anything())).thenResolve(server.object); + when(jupyterExecution.connectToNotebookServer(anything(), anything())).thenResolve(server.object); server.setup(s => s.execute(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAnyNumber(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { return Promise.resolve(generateCells(undefined, 'a=1', 'bar.py', 0, false, uuid())); }); diff --git a/src/test/datascience/jupyterServerManager.unit.test.ts b/src/test/datascience/jupyterServerManager.unit.test.ts deleted file mode 100644 index b9ab699bbf22..000000000000 --- a/src/test/datascience/jupyterServerManager.unit.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any max-func-body-length - -import * as path from 'path'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { IFileSystem } from '../../client/common/platform/types'; -import { IAsyncDisposableRegistry, IConfigurationService, IDataScienceSettings, IPythonSettings } from '../../client/common/types'; -import { JupyterServerManager } from '../../client/datascience/jupyter/jupyterServerManager'; -import { IJupyterExecution, INotebookServer, INotebookServerLaunchInfo, IStatusProvider } from '../../client/datascience/types'; -import { IInterpreterService, InterpreterType } from '../../client/interpreter/contracts'; - -suite('JupyterServerManager unit tests', () => { - let disposableRegistry: typemoq.IMock; - let configuration: typemoq.IMock; - let execution: typemoq.IMock; - let statusProvider: typemoq.IMock; - let interpreter: typemoq.IMock; - let currentInterpreter; - let fileSystem: typemoq.IMock; - let workspace: typemoq.IMock; - let dataScienceSettings: typemoq.IMock; - let serverManager: JupyterServerManager; - - function createTypeMoq(tag: string): typemoq.IMock { - // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class - // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 - const result: typemoq.IMock = typemoq.Mock.ofType(); - (result as any)['tag'] = tag; - result.setup((x: any) => x.then).returns(() => undefined); - return result; - } - - setup(() => { - disposableRegistry = typemoq.Mock.ofType(); - configuration = typemoq.Mock.ofType(); - execution = typemoq.Mock.ofType(); - statusProvider = typemoq.Mock.ofType(); - interpreter = typemoq.Mock.ofType(); - fileSystem = typemoq.Mock.ofType(); - workspace = typemoq.Mock.ofType(); - - // Setup our workspace - workspace - .setup(w => w.hasWorkspaceFolders) - .returns(() => true); - const ws = [{ uri: Uri.file('x') }]; - workspace - .setup(w => w.workspaceFolders) - .returns(() => ws as any); - - // Tell our file system that the directory exists - fileSystem.setup(fs => fs.directoryExists(typemoq.It.isAny())).returns(() => { return Promise.resolve(true); }); - - // Get our interpreter service set - currentInterpreter = { type: InterpreterType.Unknown }; - interpreter - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => { return Promise.resolve(currentInterpreter as any); }); - - // Get our default settings prepped - const pythonSettings = typemoq.Mock.ofType(); - dataScienceSettings = typemoq.Mock.ofType(); - dataScienceSettings.setup(d => d.useDefaultConfigForJupyter).returns(() => true); - const workspacePath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); - dataScienceSettings.setup(d => d.notebookFileRoot).returns(() => workspacePath); - pythonSettings.setup(p => p.datascience).returns(() => dataScienceSettings.object); - configuration.setup(c => c.getSettings(typemoq.It.isAny())).returns(() => pythonSettings.object); - - serverManager = new JupyterServerManager(disposableRegistry.object, configuration.object, interpreter.object, - fileSystem.object, execution.object, statusProvider.object, workspace.object); - }); - test('JupyterServerManager create new', async () => { - // Get our settings for this test configured - dataScienceSettings.setup(d => d.jupyterServerURI).returns(() => 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe'); - - // Create our fake notebook server - const fakeServer: typemoq.IMock = createTypeMoq('First Server'); - - // Set our execution - execution.setup(e => e.connectToNotebookServer(typemoq.It.isAny(), typemoq.It.isAny(), - typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())).returns(() => { - return Promise.resolve(fakeServer.object); - }).verifiable(typemoq.Times.once()); - - await serverManager.getOrCreateServer(); - - execution.verifyAll(); - }); - test('JupyterServerManager reuse existing', async () => { - // Get our settings for this test configured - dataScienceSettings.setup(d => d.jupyterServerURI).returns(() => 'local'); - - // Create our fake notebook server - const fakeServer: typemoq.IMock = createTypeMoq('First Server'); - const fakeLaunchInfo: typemoq.IMock = typemoq.Mock.ofType(); - fakeLaunchInfo.setup(li => li.uri).returns(() => undefined).verifiable(typemoq.Times.once()); // local gets set to undefined at launch - fakeLaunchInfo.setup(li => li.usingDarkTheme).returns(() => false).verifiable(typemoq.Times.once()); - const workspacePath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); - fakeLaunchInfo.setup(li => li.workingDir).returns(() => workspacePath).verifiable(typemoq.Times.once()); - fakeLaunchInfo.setup(li => li.currentInterpreter).returns(() => { return currentInterpreter as any; }).verifiable(typemoq.Times.once()); - - // Set our fake server to return this launch info - fakeServer.setup(fs => fs.getLaunchInfo()).returns(() => { - return fakeLaunchInfo.object; - }); - - // Set our execution - execution.setup(e => e.connectToNotebookServer(typemoq.It.isAny(), typemoq.It.isAny(), - typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())).returns(() => { - return Promise.resolve(fakeServer.object); - }).verifiable(typemoq.Times.once()); - - await serverManager.getOrCreateServer(); - await serverManager.getOrCreateServer(); - - // Execution should only have been called once, not twice - execution.verifyAll(); - fakeLaunchInfo.verifyAll(); - }); - test('JupyterServerManager don"t reuse existing', async () => { - // Get our settings for this test configured - dataScienceSettings.setup(d => d.jupyterServerURI).returns(() => 'local'); - - // Create our fake notebook server - const fakeServer: typemoq.IMock = createTypeMoq('First Server'); - const fakeLaunchInfo: typemoq.IMock = typemoq.Mock.ofType(); - fakeLaunchInfo.setup(li => li.uri).returns(() => undefined).verifiable(typemoq.Times.once()); // local gets set to undefined at launch - fakeLaunchInfo.setup(li => li.usingDarkTheme).returns(() => false).verifiable(typemoq.Times.once()); - const workspacePath = path.join(EXTENSION_ROOT_DIR, 'src', 'test'); // Change the ws path so we don't reuse - fakeLaunchInfo.setup(li => li.workingDir).returns(() => workspacePath).verifiable(typemoq.Times.once()); - fakeLaunchInfo.setup(li => li.currentInterpreter).returns(() => { return currentInterpreter as any; }).verifiable(typemoq.Times.never()); // Never - - // Set our fake server to return this launch info - fakeServer.setup(fs => fs.getLaunchInfo()).returns(() => { - return fakeLaunchInfo.object; - }); - - // Set our execution - execution.setup(e => e.connectToNotebookServer(typemoq.It.isAny(), typemoq.It.isAny(), - typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())).returns(() => { - return Promise.resolve(fakeServer.object); - }).verifiable(typemoq.Times.exactly(2)); // Twice - - await serverManager.getOrCreateServer(); - await serverManager.getOrCreateServer(); - - // Execution should be called twice - execution.verifyAll(); - fakeLaunchInfo.verifyAll(); - }); -}); diff --git a/src/test/datascience/jupyterVariables.unit.test.ts b/src/test/datascience/jupyterVariables.unit.test.ts index 6943c73aa42e..983b0b71fc25 100644 --- a/src/test/datascience/jupyterVariables.unit.test.ts +++ b/src/test/datascience/jupyterVariables.unit.test.ts @@ -1,19 +1,20 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - 'use strict'; -// tslint:disable:no-any max-func-body-length import { nbformat } from '@jupyterlab/coreutils'; import * as assert from 'assert'; import * as typemoq from 'typemoq'; + import { IFileSystem } from '../../client/common/platform/types'; import { Identifiers } from '../../client/datascience/constants'; import { JupyterVariables } from '../../client/datascience/jupyter/jupyterVariables'; -import { CellState, ICell, INotebookServer, INotebookServerManager } from '../../client/datascience/types'; +import { CellState, ICell, IHistoryProvider, IJupyterExecution, INotebookServer } from '../../client/datascience/types'; +// tslint:disable:no-any max-func-body-length suite('JupyterVariables', () => { - let serverManager: typemoq.IMock; + let execution: typemoq.IMock; + let historyProvider: typemoq.IMock; let fakeServer: typemoq.IMock; let jupyterVariables: JupyterVariables; let fileSystem: typemoq.IMock; @@ -55,7 +56,8 @@ suite('JupyterVariables', () => { } setup(() => { - serverManager = typemoq.Mock.ofType(); + execution = typemoq.Mock.ofType(); + historyProvider = typemoq.Mock.ofType(); // Create our fake notebook server fakeServer = createTypeMoq('Fake Server'); @@ -63,12 +65,12 @@ suite('JupyterVariables', () => { fileSystem.setup(fs => fs.readFile(typemoq.It.isAnyString())) .returns(() => Promise.resolve('test')); - jupyterVariables = new JupyterVariables(fileSystem.object, serverManager.object); + jupyterVariables = new JupyterVariables(fileSystem.object, execution.object, historyProvider.object); }); test('getVariables no server', async() => { - serverManager.setup(sm => sm.getActiveServer()).returns(() => { - return undefined; + execution.setup(sm => sm.getServer(typemoq.It.isAny())).returns(() => { + return Promise.resolve(undefined); }); fakeServer.setup(fs => fs.execute(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny(), undefined, typemoq.It.isAny())) @@ -86,8 +88,8 @@ suite('JupyterVariables', () => { // No cells, no output, no text/plain test('getVariables no cells', async() => { - serverManager.setup(sm => sm.getActiveServer()).returns(() => { - return fakeServer.object; + execution.setup(sm => sm.getServer(typemoq.It.isAny())).returns(() => { + return Promise.resolve(fakeServer.object); }); fakeServer.setup(fs => fs.execute(typemoq.It.isValue('test'), typemoq.It.isValue(Identifiers.EmptyFileName), typemoq.It.isValue(0), typemoq.It.isAnyString(), undefined, typemoq.It.isValue(true))) @@ -106,8 +108,8 @@ suite('JupyterVariables', () => { }); test('getVariables no output', async() => { - serverManager.setup(sm => sm.getActiveServer()).returns(() => { - return fakeServer.object; + execution.setup(sm => sm.getServer(typemoq.It.isAny())).returns(() => { + return Promise.resolve(fakeServer.object); }); fakeServer.setup(fs => fs.execute(typemoq.It.isValue('test'), typemoq.It.isValue(Identifiers.EmptyFileName), typemoq.It.isValue(0), typemoq.It.isAnyString(), undefined, typemoq.It.isValue(true))) @@ -126,8 +128,8 @@ suite('JupyterVariables', () => { }); test('getVariables bad mime', async() => { - serverManager.setup(sm => sm.getActiveServer()).returns(() => { - return fakeServer.object; + execution.setup(sm => sm.getServer(typemoq.It.isAny())).returns(() => { + return Promise.resolve(fakeServer.object); }); fakeServer.setup(fs => fs.execute(typemoq.It.isValue('test'), typemoq.It.isValue(Identifiers.EmptyFileName), typemoq.It.isValue(0), typemoq.It.isAnyString(), undefined, typemoq.It.isValue(true))) @@ -148,8 +150,8 @@ suite('JupyterVariables', () => { }); test('getVariables fake data', async() => { - serverManager.setup(sm => sm.getActiveServer()).returns(() => { - return fakeServer.object; + execution.setup(sm => sm.getServer(typemoq.It.isAny())).returns(() => { + return Promise.resolve(fakeServer.object); }); fakeServer.setup(fs => fs.execute(typemoq.It.isValue('test'), typemoq.It.isValue(Identifiers.EmptyFileName), typemoq.It.isValue(0), typemoq.It.isAnyString(), undefined, typemoq.It.isValue(true))) diff --git a/src/test/datascience/mockExtensions.ts b/src/test/datascience/mockExtensions.ts new file mode 100644 index 000000000000..7aded32b8ec8 --- /dev/null +++ b/src/test/datascience/mockExtensions.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { injectable } from 'inversify'; +import { Extension } from 'vscode'; + +import { IExtensions } from '../../client/common/types'; + +// tslint:disable:no-any unified-signatures + +@injectable() +export class MockExtensions implements IExtensions { + public all: Extension[] = []; + public getExtension(extensionId: string) : Extension | undefined { + return undefined; + } +} diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts index e04828695fbd..f28a486e7b1c 100644 --- a/src/test/datascience/notebook.functional.test.ts +++ b/src/test/datascience/notebook.functional.test.ts @@ -203,11 +203,11 @@ suite('Jupyter notebook tests', () => { }); } - async function createNotebookServer(useDefaultConfig: boolean, expectFailure?: boolean, useDarkTheme?: boolean): Promise { + async function createNotebookServer(useDefaultConfig: boolean, expectFailure?: boolean, usingDarkTheme?: boolean, purpose?: string): Promise { // Catch exceptions. Throw a specific assertion if the promise fails try { const testDir = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); - const server = await jupyterExecution.connectToNotebookServer(undefined, useDarkTheme ? true : false, useDefaultConfig, undefined, testDir); + const server = await jupyterExecution.connectToNotebookServer({ usingDarkTheme, useDefaultConfig, workingDir: testDir, purpose: purpose ? purpose : '1'}); if (expectFailure) { assert.ok(false, `Expected server to not be created`); } @@ -255,7 +255,7 @@ suite('Jupyter notebook tests', () => { const uri = connString as string; // We have a connection string here, so try to connect jupyterExecution to the notebook server - const server = await jupyterExecution.connectToNotebookServer(uri!, false, true); + const server = await jupyterExecution.connectToNotebookServer({ uri, useDefaultConfig: true, purpose: ''}); if (!server) { assert.fail('Failed to connect to remote server'); } @@ -459,7 +459,7 @@ suite('Jupyter notebook tests', () => { } // Try different timeouts, canceling after the timeout on each - assert.ok(await testCancelableMethod((t: CancellationToken) => jupyterExecution.connectToNotebookServer(undefined, false, true, t), 'Cancel did not cancel start after {0}ms')); + assert.ok(await testCancelableMethod((t: CancellationToken) => jupyterExecution.connectToNotebookServer(undefined, t), 'Cancel did not cancel start after {0}ms')); if (ioc.mockJupyter) { ioc.mockJupyter.setProcessDelay(undefined); @@ -467,7 +467,7 @@ suite('Jupyter notebook tests', () => { // Make sure doing normal start still works const nonCancelSource = new CancellationTokenSource(); - const server = await jupyterExecution.connectToNotebookServer(undefined, false, true, nonCancelSource.token); + const server = await jupyterExecution.connectToNotebookServer(undefined, nonCancelSource.token); assert.ok(server, 'Server not found with a cancel token that does not cancel'); // Make sure can run some code too @@ -819,23 +819,14 @@ plt.show()`, } }); - // Tests that should be running: - // - Creation - // - Failure - // - Not installed - // - Different mime types - // - Export/import - // - Auto import - // - changing directories - // - Restart - // - Error types - // Test to write after jupyter process abstraction - // - jupyter not installed - // - kernel spec not matching - // - ipykernel not installed - // - kernelspec not installed - // - startup / shutdown / restart - make uses same kernelspec. Actually should be in memory already - // - Starting with python that doesn't have jupyter and make sure it can switch to one that does - // - Starting with python that doesn't have jupyter and make sure the switch still uses a python that's close as the kernel + runTest('Server cache working', async () => { + const s1 = await createNotebookServer(true, false, false, 'same'); + const s2 = await createNotebookServer(true, false, false, 'same'); + assert.ok(s1 === s2, 'Two servers not the same when they should be'); + const s3 = await createNotebookServer(false, false, false, 'same'); + assert.ok(s1 !== s3, 'Different config should create different server'); + const s4 = await createNotebookServer(true, false, false, 'different'); + assert.ok(s1 !== s4, 'Different purpose should create different server'); + }); });