From 0695fef6dcd60dc68f9b849779a25dba93dc073c Mon Sep 17 00:00:00 2001 From: Adam Ward Date: Fri, 25 Jul 2025 08:01:18 -0400 Subject: [PATCH 1/7] Unified logging setup for Swift extension --- package-lock.json | 493 +++++++++++++++++- package.json | 18 +- src/DiagnosticsManager.ts | 4 +- src/FolderContext.ts | 8 +- src/PackageWatcher.ts | 2 +- src/SwiftPackage.ts | 10 +- src/SwiftSnippets.ts | 2 +- src/TestExplorer/TestExplorer.ts | 10 +- src/TestExplorer/TestRunner.ts | 24 +- src/TestExplorer/TestXUnitParser.ts | 6 +- src/WorkspaceContext.ts | 22 +- src/commands.ts | 4 +- src/commands/build.ts | 10 +- src/commands/captureDiagnostics.ts | 26 +- src/commands/dependencies/unedit.ts | 8 +- src/commands/dependencies/update.ts | 2 +- src/commands/dependencies/useLocal.ts | 2 +- .../generateSourcekitConfiguration.ts | 12 +- src/configuration.ts | 3 + src/debugger/buildConfig.ts | 5 +- src/debugger/debugAdapterFactory.ts | 6 +- src/debugger/logTracker.ts | 6 +- .../DocumentationPreviewEditor.ts | 4 +- src/extension.ts | 59 ++- src/logging/OutputChannelTransport.ts | 36 ++ src/logging/RollingLog.ts | 98 ++++ src/logging/SwiftLogger.ts | 149 ++++++ src/logging/SwiftLoggerFactory.ts | 47 ++ src/logging/SwiftOutputChannel.ts | 47 ++ .../LanguageClientConfiguration.ts | 9 +- src/sourcekit-lsp/LanguageClientManager.ts | 19 +- src/tasks/TaskManager.ts | 4 +- src/tasks/TaskQueue.ts | 10 +- src/toolchain/SelectedXcodeWatcher.ts | 7 +- src/toolchain/swiftly.ts | 41 +- src/toolchain/toolchain.ts | 48 +- src/ui/SwiftOutputChannel.ts | 165 ------ src/ui/ToolchainSelection.ts | 7 +- src/ui/win32.ts | 10 +- src/utilities/utilities.ts | 4 +- .../tasks/SwiftPluginTaskProvider.test.ts | 7 +- .../testexplorer/XCTestOutputParser.test.ts | 6 +- .../ui/ProjectPanelProvider.test.ts | 7 +- .../ui/SwiftOutputChannel.test.ts | 16 +- .../utilities/testutilities.ts | 23 +- .../commands/captureDiagnostics.test.ts | 12 +- .../debugger/debugAdapterFactory.test.ts | 48 +- .../LanguageClientManager.test.ts | 15 +- .../toolchain/SelectedXcodeWatcher.test.ts | 11 +- 49 files changed, 1154 insertions(+), 438 deletions(-) create mode 100644 src/logging/OutputChannelTransport.ts create mode 100644 src/logging/RollingLog.ts create mode 100644 src/logging/SwiftLogger.ts create mode 100644 src/logging/SwiftLoggerFactory.ts create mode 100644 src/logging/SwiftOutputChannel.ts delete mode 100644 src/ui/SwiftOutputChannel.ts diff --git a/package-lock.json b/package-lock.json index bd7872087..0b1ddae44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,7 +71,8 @@ "strip-ansi": "^6.0.1", "svgo": "^4.0.0", "tsx": "^4.20.3", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "winston": "^3.17.0" }, "engines": { "vscode": "^1.88.0" @@ -347,6 +348,28 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -2086,6 +2109,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/vscode": { "version": "1.89.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.89.0.tgz", @@ -3983,6 +4013,17 @@ "node": ">=16" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3999,6 +4040,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -4009,6 +4061,23 @@ "color-support": "bin.js" } }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -4016,6 +4085,17 @@ "dev": true, "license": "MIT" }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4783,6 +4863,13 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -5429,6 +5516,13 @@ "pend": "~1.2.0" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5513,6 +5607,13 @@ "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true, + "license": "MIT" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -6638,6 +6739,13 @@ "prebuild-install": "^7.0.1" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true, + "license": "MIT" + }, "node_modules/lazystream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", @@ -7139,6 +7247,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -8092,6 +8218,16 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -9107,6 +9243,16 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9326,6 +9472,23 @@ "url": "https://github.com/steveukx/git-js?sponsor=1" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/sinon": { "version": "21.0.0", "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz", @@ -9544,6 +9707,16 @@ "node": ">=8" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -10110,6 +10283,13 @@ "b4a": "^1.6.4" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true, + "license": "MIT" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -10180,6 +10360,16 @@ "node": ">=8.0" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -10608,6 +10798,87 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -11088,6 +11359,23 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true + }, + "@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dev": true, + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -12316,6 +12604,12 @@ "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", "dev": true }, + "@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true + }, "@types/vscode": { "version": "1.89.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.89.0.tgz", @@ -13640,6 +13934,33 @@ "integrity": "sha512-5yARKww0dWyWg2/3xZeXgoxjHLwpVqFptj9Zy7qioJ6+/L0ARM184sgMUrQDjxw7ePJWlGhV998mKhzrxT0/Kg==", "dev": true }, + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dev": true, + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + } + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -13653,6 +13974,16 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -13665,6 +13996,16 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dev": true, + "requires": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -14215,6 +14556,12 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "dev": true + }, "encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -14698,6 +15045,12 @@ "pend": "~1.2.0" } }, + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -14759,6 +15112,12 @@ "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true + }, "foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -15569,6 +15928,12 @@ "prebuild-install": "^7.0.1" } }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true + }, "lazystream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", @@ -15913,6 +16278,20 @@ } } }, + "logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dev": true, + "requires": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, "loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -16614,6 +16993,15 @@ "wrappy": "1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "requires": { + "fn.name": "1.x.x" + } + }, "onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -17302,6 +17690,12 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -17446,6 +17840,23 @@ "debug": "^4.4.0" } }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, "sinon": { "version": "21.0.0", "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz", @@ -17607,6 +18018,12 @@ } } }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true + }, "stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -18019,6 +18436,12 @@ "b4a": "^1.6.4" } }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -18070,6 +18493,12 @@ "is-number": "^7.0.0" } }, + "triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true + }, "ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -18383,6 +18812,68 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "dev": true, + "requires": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dev": true, + "requires": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/package.json b/package.json index 71ce5f3ef..886408c05 100644 --- a/package.json +++ b/package.json @@ -584,6 +584,18 @@ "default": ".build/attachments", "markdownDescription": "The path to a directory that will be used to store attachments produced during a test run.\n\nA relative path resolves relative to the root directory of the workspace running the test(s)", "scope": "machine-overridable" + }, + "swift.outputChannelLevel": { + "type": "string", + "default": "info", + "markdownDescription": "The the level of the Swift output channel. This has no affect on the verbosity of messages written to the extension's log file.", + "enum": [ + "debug", + "info", + "warn", + "error" + ], + "scope": "machine-overridable" } } }, @@ -933,7 +945,8 @@ "swift.diagnostics": { "type": "boolean", "default": false, - "markdownDescription": "Output additional diagnostics to the Swift Output View.", + "markdownDescription": "Output additional diagnostics to the Swift output channel.", + "deprecationMessage": "**Deprecated**: Please use `#swift.outputChannelLevel#` instead.", "order": 100, "scope": "machine-overridable" } @@ -1872,7 +1885,8 @@ "strip-ansi": "^6.0.1", "svgo": "^4.0.0", "tsx": "^4.20.3", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "winston": "^3.17.0" }, "dependencies": { "@vscode/codicons": "^0.0.38", diff --git a/src/DiagnosticsManager.ts b/src/DiagnosticsManager.ts index c7e0914a2..11133c9b5 100644 --- a/src/DiagnosticsManager.ts +++ b/src/DiagnosticsManager.ts @@ -99,9 +99,7 @@ export class DiagnosticsManager implements vscode.Disposable { ); }); }) - .catch(e => - context.outputChannel.log(`${e}`, 'Failed to provide "swiftc" diagnostics') - ); + .catch(e => context.logger.error(`Failed to provide "swiftc" diagnostics: ${e}`)); }); const fileTypes = validFileTypes.join(","); this.workspaceFileWatcher = vscode.workspace.createFileSystemWatcher( diff --git a/src/FolderContext.ts b/src/FolderContext.ts index 1d0b32c50..127a9df6c 100644 --- a/src/FolderContext.ts +++ b/src/FolderContext.ts @@ -22,8 +22,8 @@ import { WorkspaceContext, FolderOperation } from "./WorkspaceContext"; import { BackgroundCompilation } from "./BackgroundCompilation"; import { TaskQueue } from "./tasks/TaskQueue"; import { isPathInsidePath } from "./utilities/filesystem"; -import { SwiftOutputChannel } from "./ui/SwiftOutputChannel"; import { SwiftToolchain } from "./toolchain/toolchain"; +import { SwiftLogger } from "./logging/SwiftLogger"; export class FolderContext implements vscode.Disposable { private packageWatcher: PackageWatcher; @@ -96,7 +96,7 @@ export class FolderContext implements vscode.Disposable { void vscode.window.showErrorMessage( `Failed to load ${folderContext.name}/Package.swift: ${error.message}` ); - workspaceContext.outputChannel.log( + workspaceContext.logger.info( `Failed to load Package.swift: ${error.message}`, folderContext.name ); @@ -145,8 +145,8 @@ export class FolderContext implements vscode.Disposable { } /** Load Swift Plugins and store in Package */ - async loadSwiftPlugins(outputChannel: SwiftOutputChannel) { - await this.swiftPackage.loadSwiftPlugins(this.toolchain, outputChannel); + async loadSwiftPlugins(logger: SwiftLogger) { + await this.swiftPackage.loadSwiftPlugins(this.toolchain, logger); } /** diff --git a/src/PackageWatcher.ts b/src/PackageWatcher.ts index 90addfa8d..5bf7a366a 100644 --- a/src/PackageWatcher.ts +++ b/src/PackageWatcher.ts @@ -155,7 +155,7 @@ export class PackageWatcher { return Version.fromString(contents.toString().trim()); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { - this.workspaceContext.outputChannel.appendLine( + this.workspaceContext.logger.error( `Failed to read .swift-version file at ${versionFile}: ${error}` ); } diff --git a/src/SwiftPackage.ts b/src/SwiftPackage.ts index 905ea3da4..1824ffe1e 100644 --- a/src/SwiftPackage.ts +++ b/src/SwiftPackage.ts @@ -19,8 +19,8 @@ import { execSwift, getErrorDescription, hashString } from "./utilities/utilitie import { isPathInsidePath } from "./utilities/filesystem"; import { SwiftToolchain } from "./toolchain/toolchain"; import { BuildFlags } from "./toolchain/BuildFlags"; -import { SwiftOutputChannel } from "./ui/SwiftOutputChannel"; import { lineBreakRegex } from "./utilities/tasks"; +import { SwiftLogger } from "./logging/SwiftLogger"; /** Swift Package Manager contents */ interface PackageContents { @@ -312,7 +312,7 @@ export class SwiftPackage { private static async loadPlugins( folder: vscode.Uri, toolchain: SwiftToolchain, - outputChannel: SwiftOutputChannel + logger: SwiftLogger ): Promise { try { const { stdout } = await execSwift(["package", "plugin", "--list"], toolchain, { @@ -333,7 +333,7 @@ export class SwiftPackage { } return plugins; } catch (error) { - outputChannel.appendLine(`Failed to laod plugins: ${error}`); + logger.error(`Failed to load plugins: ${error}`); // failed to load resolved file return undefined return []; } @@ -375,8 +375,8 @@ export class SwiftPackage { this.workspaceState = await SwiftPackage.loadWorkspaceState(this.folder); } - public async loadSwiftPlugins(toolchain: SwiftToolchain, outputChannel: SwiftOutputChannel) { - this.plugins = await SwiftPackage.loadPlugins(this.folder, toolchain, outputChannel); + public async loadSwiftPlugins(toolchain: SwiftToolchain, logger: SwiftLogger) { + this.plugins = await SwiftPackage.loadPlugins(this.folder, toolchain, logger); } /** Return if has valid contents */ diff --git a/src/SwiftSnippets.ts b/src/SwiftSnippets.ts index 375190e96..d6aff6e13 100644 --- a/src/SwiftSnippets.ts +++ b/src/SwiftSnippets.ts @@ -120,7 +120,7 @@ export async function debugSnippetWithOptions( return result; }); } catch (error) { - ctx.outputChannel.appendLine(`Failed to debug snippet: ${error}`); + ctx.logger.error(`Failed to debug snippet: ${error}`); // ignore error if task failed to run return false; } diff --git a/src/TestExplorer/TestExplorer.ts b/src/TestExplorer/TestExplorer.ts index 0a447ba08..4320f55ac 100644 --- a/src/TestExplorer/TestExplorer.ts +++ b/src/TestExplorer/TestExplorer.ts @@ -270,7 +270,7 @@ export class TestExplorer { // we fall back to discovering tests with SPM. await this.discoverTestsInWorkspaceLSP(token); } catch { - this.folderContext.workspaceContext.outputChannel.logDiagnostic( + this.folderContext.workspaceContext.logger.debug( "workspace/tests LSP request not supported, falling back to SPM to discover tests.", "Test Discovery" ); @@ -300,7 +300,7 @@ export class TestExplorer { ) .then(selected => { if (selected === enable) { - explorer.folderContext.workspaceContext.outputChannel.log( + explorer.folderContext.workspaceContext.logger.info( `Enabling SourceKit-LSP after swift-testing message` ); void vscode.workspace @@ -310,7 +310,7 @@ export class TestExplorer { /* Put in worker queue */ }); } else if (selected === ok) { - explorer.folderContext.workspaceContext.outputChannel.log( + explorer.folderContext.workspaceContext.logger.info( `User acknowledged that SourceKit-LSP is disabled` ); } @@ -399,7 +399,7 @@ export class TestExplorer { } else { explorer.setErrorTestItem(errorDescription); } - explorer.folderContext.workspaceContext.outputChannel.log( + explorer.folderContext.workspaceContext.logger.error( `Test Discovery Failed: ${errorDescription}`, explorer.folderContext.name ); @@ -439,7 +439,7 @@ export class TestExplorer { * @param errorDescription Error description to display */ private setErrorTestItem(errorDescription: string | undefined, title = "Test Discovery Error") { - this.folderContext.workspaceContext.outputChannel.log( + this.folderContext.workspaceContext.logger.error( `Test Discovery Error: ${errorDescription}` ); this.controller.items.forEach(item => { diff --git a/src/TestExplorer/TestRunner.ts b/src/TestExplorer/TestRunner.ts index 39b4e801b..b3b08ee2a 100644 --- a/src/TestExplorer/TestRunner.ts +++ b/src/TestExplorer/TestRunner.ts @@ -658,7 +658,7 @@ export class TestRunner { await this.runSession(runState); } } catch (error) { - this.workspaceContext.outputChannel.log(`Error: ${getErrorDescription(error)}`); + this.workspaceContext.logger.error(`Error: ${getErrorDescription(error)}`); this.testRun.appendOutput(`\r\nError: ${getErrorDescription(error)}`); } @@ -726,7 +726,7 @@ export class TestRunner { await SwiftTestingConfigurationSetup.cleanupAttachmentFolder( this.folderContext, testRunTime, - this.workspaceContext.outputChannel + this.workspaceContext.logger ); }); } @@ -923,11 +923,7 @@ export class TestRunner { const xUnitParser = new TestXUnitParser( this.folderContext.toolchain.hasMultiLineParallelTestOutput ); - const results = await xUnitParser.parse( - buffer, - runState, - this.workspaceContext.outputChannel - ); + const results = await xUnitParser.parse(buffer, runState, this.workspaceContext.logger); if (results) { this.testRun.appendOutput( `\r\nExecuted ${results.tests} tests, with ${results.failures} failures and ${results.errors} errors.\r\n` @@ -1007,7 +1003,7 @@ export class TestRunner { // output test build configuration if (configuration.diagnostics) { const configJSON = JSON.stringify(swiftTestBuildConfig); - this.workspaceContext.outputChannel.logDiagnostic( + this.workspaceContext.logger.debug( `swift-testing Debug Config: ${configJSON}`, this.folderContext.name ); @@ -1034,7 +1030,7 @@ export class TestRunner { // output test build configuration if (configuration.diagnostics) { const configJSON = JSON.stringify(xcTestBuildConfig); - this.workspaceContext.outputChannel.logDiagnostic( + this.workspaceContext.logger.debug( `XCTest Debug Config: ${configJSON}`, this.folderContext.name ); @@ -1062,7 +1058,7 @@ export class TestRunner { LoggingDebugAdapterTracker.setDebugSessionCallback( session, - this.workspaceContext.outputChannel, + this.workspaceContext.logger, output => { outputHandler(output); } @@ -1070,7 +1066,7 @@ export class TestRunner { // add cancellation const cancellation = this.testRun.token.onCancellationRequested(() => { - this.workspaceContext.outputChannel.logDiagnostic( + this.workspaceContext.logger.debug( "Test Debugging Cancelled", this.folderContext.name ); @@ -1084,7 +1080,7 @@ export class TestRunner { if (e.name !== config.name) { return; } - this.workspaceContext.outputChannel.logDiagnostic( + this.workspaceContext.logger.debug( "Stop Test Debugging", this.folderContext.name ); @@ -1113,7 +1109,7 @@ export class TestRunner { this.testRun.testRunStarted(); } - this.workspaceContext.outputChannel.logDiagnostic( + this.workspaceContext.logger.debug( "Start Test Debugging", this.folderContext.name ); @@ -1137,7 +1133,7 @@ export class TestRunner { await SwiftTestingConfigurationSetup.cleanupAttachmentFolder( this.folderContext, testRunTime, - this.workspaceContext.outputChannel + this.workspaceContext.logger ); }); } diff --git a/src/TestExplorer/TestXUnitParser.ts b/src/TestExplorer/TestXUnitParser.ts index 8a4e19062..efbd27253 100644 --- a/src/TestExplorer/TestXUnitParser.ts +++ b/src/TestExplorer/TestXUnitParser.ts @@ -14,7 +14,7 @@ import * as xml2js from "xml2js"; import { ITestRunState } from "./TestParsers/TestRunState"; -import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; +import { SwiftLogger } from "../logging/SwiftLogger"; export interface TestResults { tests: number; @@ -50,14 +50,14 @@ export class TestXUnitParser { async parse( buffer: string, runState: ITestRunState, - outputChannel: SwiftOutputChannel + logger: SwiftLogger ): Promise { const xml = await xml2js.parseStringPromise(buffer); try { return await this.parseXUnit(xml, runState); } catch (error) { // ignore error - outputChannel.appendLine(`Error parsing xUnit output: ${error}`); + logger.error(`Error parsing xUnit output: ${error}`); return undefined; } } diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index c8eb004c5..f9b9e115e 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -16,7 +16,6 @@ import * as vscode from "vscode"; import * as path from "path"; import { FolderContext } from "./FolderContext"; import { StatusItem } from "./ui/StatusItem"; -import { SwiftOutputChannel } from "./ui/SwiftOutputChannel"; import { swiftLibraryPathKey } from "./utilities/utilities"; import { isExcluded, isPathInsidePath } from "./utilities/filesystem"; import { LanguageClientToolchainCoordinator } from "./sourcekit-lsp/LanguageClientToolchainCoordinator"; @@ -37,6 +36,8 @@ import { isValidWorkspaceFolder, searchForPackages } from "./utilities/workspace import { SwiftPluginTaskProvider } from "./tasks/SwiftPluginTaskProvider"; import { SwiftTaskProvider } from "./tasks/SwiftTaskProvider"; import { LLDBDebugConfigurationProvider } from "./debugger/debugAdapterFactory"; +import { SwiftLogger } from "./logging/SwiftLogger"; +import { SwiftLoggerFactory } from "./logging/SwiftLoggerFactory"; /** * Context for whole workspace. Holds array of contexts for each workspace folder @@ -71,12 +72,15 @@ export class WorkspaceContext implements vscode.Disposable { public onDidStartBuild = this.buildStartEmitter.event; public onDidFinishBuild = this.buildFinishEmitter.event; + public loggerFactory: SwiftLoggerFactory; + private constructor( extensionContext: vscode.ExtensionContext, public tempFolder: TemporaryFolder, - public outputChannel: SwiftOutputChannel, + public logger: SwiftLogger, public globalToolchain: SwiftToolchain ) { + this.loggerFactory = new SwiftLoggerFactory(extensionContext.logUri); this.statusItem = new StatusItem(); this.buildStatus = new SwiftBuildStatus(this.statusItem); this.languageClientManager = new LanguageClientToolchainCoordinator(this, { @@ -88,11 +92,7 @@ export class WorkspaceContext implements vscode.Disposable { this.diagnostics = new DiagnosticsManager(this); this.taskProvider = new SwiftTaskProvider(this); this.pluginProvider = new SwiftPluginTaskProvider(this); - this.launchProvider = new LLDBDebugConfigurationProvider( - process.platform, - this, - outputChannel - ); + this.launchProvider = new LLDBDebugConfigurationProvider(process.platform, this, logger); this.documentation = new DocumentationManager(extensionContext, this); this.currentDocument = null; this.commentCompletionProvider = new CommentCompletionProviders(); @@ -199,7 +199,7 @@ export class WorkspaceContext implements vscode.Disposable { this.diagnostics, this.documentation, this.languageClientManager, - this.outputChannel, + this.logger, this.statusItem, this.buildStatus, ]; @@ -230,11 +230,11 @@ export class WorkspaceContext implements vscode.Disposable { /** Get swift version and create WorkspaceContext */ static async create( extensionContext: vscode.ExtensionContext, - outputChannel: SwiftOutputChannel, + logger: SwiftLogger, toolchain: SwiftToolchain ): Promise { const tempFolder = await TemporaryFolder.create(); - return new WorkspaceContext(extensionContext, tempFolder, outputChannel, toolchain); + return new WorkspaceContext(extensionContext, tempFolder, logger, toolchain); } /** @@ -446,7 +446,7 @@ export class WorkspaceContext implements vscode.Disposable { // find context with root folder const index = this.folders.findIndex(context => context.folder.fsPath === folder.fsPath); if (index !== -1) { - this.outputChannel.log(`Adding package folder ${folder} twice`, "WARN"); + this.logger.warn(`Adding package folder ${folder} twice`); return this.folders[index]; } const folderContext = await FolderContext.create(folder, workspaceFolder, this); diff --git a/src/commands.ts b/src/commands.ts index caf51d04b..1bff21c76 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -50,6 +50,7 @@ import restartLSPServer from "./commands/restartLSPServer"; import { generateLaunchConfigurations } from "./commands/generateLaunchConfigurations"; import { runTest } from "./commands/runTest"; import { generateSourcekitConfiguration } from "./commands/generateSourcekitConfiguration"; +import { SwiftLogger } from "./logging/SwiftLogger"; /** * References: @@ -64,6 +65,7 @@ export type WorkspaceContextWithToolchain = WorkspaceContext & { toolchain: Swif export function registerToolchainCommands( toolchain: SwiftToolchain | undefined, + logger: SwiftLogger, cwd?: vscode.Uri ): vscode.Disposable[] { return [ @@ -71,7 +73,7 @@ export function registerToolchainCommands( createNewProject(toolchain) ), vscode.commands.registerCommand("swift.selectToolchain", () => - showToolchainSelectionQuickPick(toolchain, cwd) + showToolchainSelectionQuickPick(toolchain, logger, cwd) ), vscode.commands.registerCommand("swift.pickProcess", configuration => pickProcess(configuration) diff --git a/src/commands/build.ts b/src/commands/build.ts index a0d3ee7cd..e88a496bb 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -77,9 +77,7 @@ export async function debugBuildWithOptions( ) { const current = ctx.currentFolder; if (!current) { - ctx.outputChannel.appendLine( - "debugBuildWithOptions: No current folder on WorkspaceContext" - ); + ctx.logger.debug("debugBuildWithOptions: No current folder on WorkspaceContext"); return; } @@ -90,7 +88,7 @@ export async function debugBuildWithOptions( } else { const file = vscode.window.activeTextEditor?.document.fileName; if (!file) { - ctx.outputChannel.appendLine("debugBuildWithOptions: No active text editor"); + ctx.logger.debug("debugBuildWithOptions: No active text editor"); return; } @@ -98,12 +96,12 @@ export async function debugBuildWithOptions( } if (!target) { - ctx.outputChannel.appendLine("debugBuildWithOptions: No active target"); + ctx.logger.debug("debugBuildWithOptions: No active target"); return; } if (target.type !== "executable") { - ctx.outputChannel.appendLine( + ctx.logger.debug( `debugBuildWithOptions: Target is not an executable, instead is ${target.type}` ); return; diff --git a/src/commands/captureDiagnostics.ts b/src/commands/captureDiagnostics.ts index 8869eae8a..ac5a79868 100644 --- a/src/commands/captureDiagnostics.ts +++ b/src/commands/captureDiagnostics.ts @@ -44,7 +44,7 @@ export async function captureDiagnostics( ); await fsPromises.mkdir(diagnosticsDir); - await writeLogFile(diagnosticsDir, "extension-logs.txt", extensionLogs(ctx)); + await copyLogFile(diagnosticsDir, extensionLogFile(ctx)); const singleFolderWorkspace = ctx.folders.length === 1; const zipDir = await createDiagnosticsZipDir(); @@ -71,11 +71,10 @@ export async function captureDiagnostics( if (folder.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0))) { await sourcekitDiagnose(folder, outputDir); } else { - await writeLogFile( - outputDir, - `${baseName}-${guid}-sourcekit-lsp.txt`, - sourceKitLogs(folder) - ); + const logFile = sourceKitLogFile(folder); + if (logFile) { + await copyLogFile(outputDir, logFile); + } } } } @@ -87,7 +86,7 @@ export async function captureDiagnostics( // Clean up the diagnostics directory, leaving `zipFilePath` with the zip file. await fsPromises.rm(diagnosticsDir, { recursive: true, force: true }); - ctx.outputChannel.log(`Saved diagnostics to ${zipFilePath}`); + ctx.logger.info(`Saved diagnostics to ${zipFilePath}`); await showCapturedDiagnosticsResults(zipFilePath); return zipFilePath; @@ -208,6 +207,10 @@ async function writeLogFile(dir: string, name: string, logs: string) { await fsPromises.writeFile(path.join(dir, name), logs); } +async function copyLogFile(dir: string, filePath: string) { + await fsPromises.copyFile(filePath, path.join(dir, path.basename(filePath))); +} + /** * Creates a directory for diagnostics zip files, located in the system's temporary directory. */ @@ -217,8 +220,8 @@ async function createDiagnosticsZipDir(): Promise { return diagnosticsDir; } -function extensionLogs(ctx: WorkspaceContext): string { - return ctx.outputChannel.logs.join("\n"); +function extensionLogFile(ctx: WorkspaceContext): string { + return ctx.logger.logFilePath; } function settingsLogs(ctx: FolderContext): string { @@ -239,10 +242,9 @@ function diagnosticLogs(): string { .join("\n"); } -function sourceKitLogs(folder: FolderContext) { +function sourceKitLogFile(folder: FolderContext) { const languageClient = folder.workspaceContext.languageClientManager.get(folder); - const logs = languageClient.languageClientOutputChannel?.logs ?? []; - return logs.join("\n"); + return languageClient.languageClientOutputChannel?.logFilePath; } async function sourcekitDiagnose(ctx: FolderContext, dir: string) { diff --git a/src/commands/dependencies/unedit.ts b/src/commands/dependencies/unedit.ts index 4299f1b87..f254298b6 100644 --- a/src/commands/dependencies/unedit.ts +++ b/src/commands/dependencies/unedit.ts @@ -30,10 +30,10 @@ export async function uneditDependency( ) { const currentFolder = folder ?? ctx.currentFolder; if (!currentFolder) { - ctx.outputChannel.log("currentFolder is not set."); + ctx.logger.debug("currentFolder is not set."); return false; } - ctx.outputChannel.log(`unedit dependency ${identifier}`, currentFolder.name); + ctx.logger.debug(`unedit dependency ${identifier}`, currentFolder.name); const status = `Reverting edited dependency ${identifier} (${currentFolder.name})`; return await ctx.statusItem.showStatusWhileRunning(status, async () => { return await uneditFolderDependency(currentFolder, identifier, ctx); @@ -89,12 +89,12 @@ async function uneditFolderDependency( ); if (result === "No") { - ctx.outputChannel.log(execError.stderr, folder.name); + ctx.logger.error(execError.stderr, folder.name); return false; } await uneditFolderDependency(folder, identifier, ctx, ["--force"]); } else { - ctx.outputChannel.log(execError.stderr, folder.name); + ctx.logger.error(execError.stderr, folder.name); void vscode.window.showErrorMessage(`${execError.stderr}`); } return false; diff --git a/src/commands/dependencies/update.ts b/src/commands/dependencies/update.ts index 440eb2d59..86325ce4b 100644 --- a/src/commands/dependencies/update.ts +++ b/src/commands/dependencies/update.ts @@ -25,7 +25,7 @@ import { packageName } from "../../utilities/tasks"; export async function updateDependencies(ctx: WorkspaceContext) { const current = ctx.currentFolder; if (!current) { - ctx.outputChannel.log("currentFolder is not set."); + ctx.logger.debug("currentFolder is not set.", "updateDependencies"); return false; } return await updateFolderDependencies(current); diff --git a/src/commands/dependencies/useLocal.ts b/src/commands/dependencies/useLocal.ts index e8cc1db71..d4256d203 100644 --- a/src/commands/dependencies/useLocal.ts +++ b/src/commands/dependencies/useLocal.ts @@ -32,7 +32,7 @@ export async function useLocalDependency( ): Promise { const currentFolder = ctx.currentFolder; if (!currentFolder) { - ctx.outputChannel.log("currentFolder is not set."); + ctx.logger.debug("currentFolder is not set.", "useLocalDependency"); return false; } let folder = dep; diff --git a/src/commands/generateSourcekitConfiguration.ts b/src/commands/generateSourcekitConfiguration.ts index 3a6582d79..ed44b679c 100644 --- a/src/commands/generateSourcekitConfiguration.ts +++ b/src/commands/generateSourcekitConfiguration.ts @@ -62,7 +62,7 @@ async function createSourcekitConfiguration( return true; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { - workspaceContext.outputChannel.appendLine( + workspaceContext.logger.error( `Failed to read file at ${sourcekitConfigFile.fsPath}: ${error}` ); } @@ -79,7 +79,7 @@ async function createSourcekitConfiguration( } } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { - workspaceContext.outputChannel.appendLine( + workspaceContext.logger.error( `Failed to read folder at ${sourcekitFolder.fsPath}: ${error}` ); } @@ -148,9 +148,7 @@ async function checkDocumentSchema(doc: vscode.TextDocument, workspaceContext: W buffer = await vscode.workspace.fs.readFile(doc.uri); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { - workspaceContext.outputChannel.appendLine( - `Failed to read file at ${doc.uri.fsPath}: ${error}` - ); + workspaceContext.logger.error(`Failed to read file at ${doc.uri.fsPath}: ${error}`); } return; } @@ -159,9 +157,7 @@ async function checkDocumentSchema(doc: vscode.TextDocument, workspaceContext: W const contents = Buffer.from(buffer).toString("utf-8"); config = JSON.parse(contents); } catch (error) { - workspaceContext.outputChannel.appendLine( - `Failed to parse JSON from ${doc.uri.fsPath}: ${error}` - ); + workspaceContext.logger.error(`Failed to parse JSON from ${doc.uri.fsPath}: ${error}`); return; } const schema = config.$schema; diff --git a/src/configuration.ts b/src/configuration.ts index 89358d705..774d831c7 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -522,6 +522,9 @@ const configuration = { /* Put in worker queue */ }); }, + get outputChannelLevel(): string { + return vscode.workspace.getConfiguration("swift").get("outputChannelLevel", "info"); + }, }; const vsCodeVariableRegex = new RegExp(/\$\{(.+?)\}/g); diff --git a/src/debugger/buildConfig.ts b/src/debugger/buildConfig.ts index 9cd930737..738f5d00f 100644 --- a/src/debugger/buildConfig.ts +++ b/src/debugger/buildConfig.ts @@ -28,6 +28,7 @@ import { TestKind, isDebugging, isRelease } from "../TestExplorer/TestKind"; import { buildOptions } from "../tasks/SwiftTaskProvider"; import { updateLaunchConfigForCI } from "./lldb"; import { packageName } from "../utilities/tasks"; +import { SwiftLogger } from "../logging/SwiftLogger"; export class BuildConfigurationFactory { public static buildAll( @@ -136,7 +137,7 @@ export class SwiftTestingConfigurationSetup { public static async cleanupAttachmentFolder( folderContext: FolderContext, testRunTime: number, - outputChannel: vscode.OutputChannel + logger: SwiftLogger ): Promise { const attachmentPath = SwiftTestingConfigurationSetup.resolveAttachmentPath( folderContext, @@ -153,7 +154,7 @@ export class SwiftTestingConfigurationSetup { await fs.rmdir(attachmentPath); } } catch (error) { - outputChannel.appendLine(`Failed to clean up attachment path: ${error}`); + logger.error(`Failed to clean up attachment path: ${error}`); } } } diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index d132df6c4..28abb7cde 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -18,11 +18,11 @@ import { WorkspaceContext } from "../WorkspaceContext"; import { DebugAdapter, LaunchConfigType, SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter"; import { registerLoggingDebugAdapterTracker } from "./logTracker"; import { SwiftToolchain } from "../toolchain/toolchain"; -import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; import { fileExists } from "../utilities/filesystem"; import { updateLaunchConfigForCI, getLLDBLibPath } from "./lldb"; import { getErrorDescription, swiftRuntimeEnv } from "../utilities/utilities"; import configuration from "../configuration"; +import { SwiftLogger } from "../logging/SwiftLogger"; /** * Registers the active debugger with the extension, and reregisters it @@ -87,7 +87,7 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration constructor( private platform: NodeJS.Platform, private workspaceContext: WorkspaceContext, - private outputChannel: SwiftOutputChannel + private logger: SwiftLogger ) {} async resolveDebugConfigurationWithSubstitutedVariables( @@ -210,7 +210,7 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration void vscode.window.showWarningMessage( `Failed to setup CodeLLDB for debugging of Swift code. Debugging may produce unexpected results. ${errorMessage}` ); - this.outputChannel.log(`Failed to setup CodeLLDB: ${errorMessage}`); + this.logger.error(`Failed to setup CodeLLDB: ${errorMessage}`); return true; } const libLldbPath = libLldbPathResult.success; diff --git a/src/debugger/logTracker.ts b/src/debugger/logTracker.ts index 364082450..df6004c96 100644 --- a/src/debugger/logTracker.ts +++ b/src/debugger/logTracker.ts @@ -14,7 +14,7 @@ import * as vscode from "vscode"; import { LaunchConfigType } from "./debugAdapter"; -import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; +import { SwiftLogger } from "../logging/SwiftLogger"; /** * Factory class for building LoggingDebugAdapterTracker @@ -76,7 +76,7 @@ export class LoggingDebugAdapterTracker implements vscode.DebugAdapterTracker { static setDebugSessionCallback( session: vscode.DebugSession, - outputChannel: SwiftOutputChannel, + logger: SwiftLogger, cb: (log: string) => void ) { const loggingDebugAdapter = this.debugSessionIdMap[session.id]; @@ -87,7 +87,7 @@ export class LoggingDebugAdapterTracker implements vscode.DebugAdapterTracker { } loggingDebugAdapter.output = []; } else { - outputChannel.appendLine("Could not find debug adapter for session: " + session.id); + logger.error("Could not find debug adapter for session: " + session.id); } } diff --git a/src/documentation/DocumentationPreviewEditor.ts b/src/documentation/DocumentationPreviewEditor.ts index 2ffa13b03..dffaa4213 100644 --- a/src/documentation/DocumentationPreviewEditor.ts +++ b/src/documentation/DocumentationPreviewEditor.ts @@ -246,13 +246,13 @@ export class DocumentationPreviewEditor implements vscode.Disposable { break; default: // We should log additional info for other response errors - this.context.outputChannel.log( + this.context.logger.error( baseLogErrorMessage + JSON.stringify(error.toJson(), undefined, 2) ); break; } } else { - this.context.outputChannel.log(baseLogErrorMessage + `${error}`); + this.context.logger.error(baseLogErrorMessage + `${error}`); } this.postMessage({ type: "update-content", diff --git a/src/extension.ts b/src/extension.ts index 7445cd9d0..abb42ba1a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -29,7 +29,6 @@ import { getReadOnlyDocumentProvider } from "./ui/ReadOnlyDocumentProvider"; import { registerDebugger } from "./debugger/debugAdapterFactory"; import { showToolchainError } from "./ui/ToolchainSelection"; import { SwiftToolchain } from "./toolchain/toolchain"; -import { SwiftOutputChannel } from "./ui/SwiftOutputChannel"; import { checkAndWarnAboutWindowsSymlinks } from "./ui/win32"; import { SwiftEnvironmentVariablesManager, SwiftTerminalProfileProvider } from "./terminal"; import { resolveFolderDependencies } from "./commands/dependencies/resolve"; @@ -37,6 +36,8 @@ import { SelectedXcodeWatcher } from "./toolchain/SelectedXcodeWatcher"; import configuration, { handleConfigurationChangeEvent } from "./configuration"; import contextKeys from "./contextKeys"; import { registerSourceKitSchemaWatcher } from "./commands/generateSourcekitConfiguration"; +import { SwiftLogger } from "./logging/SwiftLogger"; +import { SwiftLoggerFactory } from "./logging/SwiftLoggerFactory"; /** * External API as exposed by the extension. Can be queried by other extensions @@ -44,7 +45,7 @@ import { registerSourceKitSchemaWatcher } from "./commands/generateSourcekitConf */ export interface Api { workspaceContext?: WorkspaceContext; - outputChannel: SwiftOutputChannel; + logger: SwiftLogger; activate(): Promise; deactivate(): Promise; } @@ -54,13 +55,17 @@ export interface Api { */ export async function activate(context: vscode.ExtensionContext): Promise { try { - const outputChannel = new SwiftOutputChannel("Swift"); - context.subscriptions.push(outputChannel); - outputChannel.log("Activating Swift for Visual Studio Code..."); + await vscode.workspace.fs.createDirectory(context.logUri); + const logger = new SwiftLoggerFactory(context.logUri).create( + "Swift", + "swift-vscode-extension.log" + ); + context.subscriptions.push(logger); + logger.info("Activating Swift for Visual Studio Code..."); - checkAndWarnAboutWindowsSymlinks(outputChannel); + checkAndWarnAboutWindowsSymlinks(logger); - const toolchain = await createActiveToolchain(outputChannel); + const toolchain = await createActiveToolchain(logger); // If we don't have a toolchain, show an error and stop initializing the extension. // This can happen if the user has not installed Swift or if the toolchain is not @@ -69,7 +74,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { void showToolchainError(); return { workspaceContext: undefined, - outputChannel, + logger, activate: () => activate(context), deactivate: async () => { await deactivate(context); @@ -77,13 +82,17 @@ export async function activate(context: vscode.ExtensionContext): Promise { }; } - const workspaceContext = await WorkspaceContext.create(context, outputChannel, toolchain); + const workspaceContext = await WorkspaceContext.create(context, logger, toolchain); context.subscriptions.push(workspaceContext); context.subscriptions.push(new SwiftEnvironmentVariablesManager(context)); context.subscriptions.push(SwiftTerminalProfileProvider.register()); context.subscriptions.push( - ...commands.registerToolchainCommands(toolchain, workspaceContext.currentFolder?.folder) + ...commands.registerToolchainCommands( + toolchain, + workspaceContext.logger, + workspaceContext.currentFolder?.folder + ) ); // Watch for configuration changes the trigger a reload of the extension if necessary. @@ -95,7 +104,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { context.subscriptions.push(...commands.register(workspaceContext)); context.subscriptions.push(registerDebugger(workspaceContext)); - context.subscriptions.push(new SelectedXcodeWatcher(outputChannel)); + context.subscriptions.push(new SelectedXcodeWatcher(logger)); // Register task provider. context.subscriptions.push( @@ -116,10 +125,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { // observer for logging workspace folder addition/removal context.subscriptions.push( workspaceContext.onDidChangeFolders(({ folder, operation }) => { - workspaceContext.outputChannel.log( - `${operation}: ${folder?.folder.fsPath}`, - folder?.name - ); + logger.info(`${operation}: ${folder?.folder.fsPath}`, folder?.name); }) ); @@ -134,9 +140,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { context.subscriptions.push(dependenciesView, projectPanelProvider); // observer that will resolve package and build launch configurations - context.subscriptions.push( - workspaceContext.onDidChangeFolders(handleFolderEvent(outputChannel)) - ); + context.subscriptions.push(workspaceContext.onDidChangeFolders(handleFolderEvent(logger))); context.subscriptions.push(TestExplorer.observeFolders(workspaceContext)); context.subscriptions.push(registerSourceKitSchemaWatcher(workspaceContext)); @@ -149,7 +153,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { return { workspaceContext, - outputChannel, + logger, activate: () => activate(context), deactivate: async () => { await workspaceContext.stop(); @@ -165,9 +169,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { } } -function handleFolderEvent( - outputChannel: SwiftOutputChannel -): (event: FolderEvent) => Promise { +function handleFolderEvent(logger: SwiftLogger): (event: FolderEvent) => Promise { // function called when a folder is added. I broke this out so we can trigger it // without having to await for it. async function folderAdded(folder: FolderContext, workspace: WorkspaceContext) { @@ -191,7 +193,7 @@ function handleFolderEvent( void workspace.statusItem.showStatusWhileRunning( `Loading Swift Plugins (${FolderContext.uriName(folder.workspaceFolder.uri)})`, async () => { - await folder.loadSwiftPlugins(outputChannel); + await folder.loadSwiftPlugins(logger); workspace.updatePluginContextKey(); await folder.fireEvent(FolderOperation.pluginsUpdated); } @@ -237,17 +239,14 @@ function handleFolderEvent( }; } -async function createActiveToolchain( - outputChannel: SwiftOutputChannel -): Promise { +async function createActiveToolchain(logger: SwiftLogger): Promise { try { - const toolchain = await SwiftToolchain.create(undefined, outputChannel); - toolchain.logDiagnostics(outputChannel); + const toolchain = await SwiftToolchain.create(undefined, logger); + toolchain.logDiagnostics(logger); contextKeys.updateKeysBasedOnActiveVersion(toolchain.swiftVersion); return toolchain; } catch (error) { - outputChannel.log("Failed to discover Swift toolchain"); - outputChannel.log(`${error}`); + logger.error(`Failed to discover Swift toolchain: ${error}`); return undefined; } } diff --git a/src/logging/OutputChannelTransport.ts b/src/logging/OutputChannelTransport.ts new file mode 100644 index 000000000..c785fcb6e --- /dev/null +++ b/src/logging/OutputChannelTransport.ts @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; +import * as Transport from "winston-transport"; + +export class OutputChannelTransport extends Transport { + private appending: boolean = false; + + constructor(private readonly ouptutChannel: vscode.OutputChannel) { + super(); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public log(info: any, next: () => void): void { + const logMessage = this.appending ? info.message : info[Symbol.for("message")]; + if (info.append) { + this.ouptutChannel.append(logMessage); + this.appending = true; + } else { + this.ouptutChannel.appendLine(logMessage); + this.appending = false; + } + next(); + } +} diff --git a/src/logging/RollingLog.ts b/src/logging/RollingLog.ts new file mode 100644 index 000000000..279b4aa10 --- /dev/null +++ b/src/logging/RollingLog.ts @@ -0,0 +1,98 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; +import * as Transport from "winston-transport"; + +export class RollingLog implements vscode.Disposable { + private _logs: (string | null)[]; + private startIndex: number = 0; + private endIndex: number = 0; + private logCount: number = 0; + + constructor(private maxLogs: number) { + this._logs = new Array(maxLogs).fill(null); + } + + public get logs(): string[] { + const logs: string[] = []; + for (let i = 0; i < this.logCount; i++) { + logs.push(this._logs[(this.startIndex + i) % this.maxLogs]!); + } + return logs; + } + + private incrementIndex(index: number): number { + return (index + 1) % this.maxLogs; + } + + dispose() { + this.clear(); + } + + clear() { + this._logs = new Array(this.maxLogs).fill(null); + this.startIndex = 0; + this.endIndex = 0; + this.logCount = 0; + } + + appendLine(log: string) { + // Writing to a new line that isn't the very first, increment the end index + if (this.logCount > 0) { + this.endIndex = this.incrementIndex(this.endIndex); + } + + // We're over the window size, move the start index + if (this.logCount === this.maxLogs) { + this.startIndex = this.incrementIndex(this.startIndex); + } else { + this.logCount++; + } + + this._logs[this.endIndex] = log; + } + + append(log: string) { + if (this.logCount === 0) { + this.logCount = 1; + } + const newLogLine = (this._logs[this.endIndex] ?? "") + log; + this._logs[this.endIndex] = newLogLine; + } + + replace(log: string) { + this._logs = new Array(this.maxLogs).fill(null); + this._logs[0] = log; + this.startIndex = 0; + this.endIndex = 1; + this.logCount = 1; + } +} + +export class RollingLogTransport extends Transport { + constructor(private readonly rollingLog: RollingLog) { + super(); + this.level = "info"; // This log is used for testing, we + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public log(info: any, next: () => void): void { + if (info.append) { + this.rollingLog.append(info.message); + } else { + this.rollingLog.appendLine(info.message); + } + next(); + } +} diff --git a/src/logging/SwiftLogger.ts b/src/logging/SwiftLogger.ts new file mode 100644 index 000000000..69812804a --- /dev/null +++ b/src/logging/SwiftLogger.ts @@ -0,0 +1,149 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import * as winston from "winston"; +import { RollingLog } from "./RollingLog"; +import { RollingLogTransport } from "./RollingLog"; +import { IS_RUNNING_UNDER_TEST } from "../utilities/utilities"; +import { OutputChannelTransport } from "./OutputChannelTransport"; +import configuration from "../configuration"; + +// Winston work off of "any" as meta data so creating this +// type so we don't have to disable ESLint many times below +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type LoggerMeta = any; +type LogMessageOptions = { append: boolean }; + +export class SwiftLogger implements vscode.Disposable { + private disposables: vscode.Disposable[] = []; + private logger: winston.Logger; + protected rollingLog: RollingLog; + protected outputChannel: vscode.OutputChannel; + + constructor( + public readonly name: string, + public readonly logFilePath: string, + logStoreLinesSize: number = 250_000 // default to capturing 250k log lines + ) { + this.rollingLog = new RollingLog(logStoreLinesSize); + this.outputChannel = vscode.window.createOutputChannel(name); + const ouptutChannelTransport = new OutputChannelTransport(this.outputChannel); + ouptutChannelTransport.level = this.outputChannelLevel; + const rollingLogTransport = new RollingLogTransport(this.rollingLog); + this.logger = winston.createLogger({ + transports: [ + new winston.transports.File({ + filename: this.logFilePath, + level: "debug", // File logging at the 'debug' level always + }), + ouptutChannelTransport, + // Only want to capture the rolling log in memory when testing + ...(IS_RUNNING_UNDER_TEST ? [rollingLogTransport] : []), + ], + format: winston.format.combine( + winston.format.errors({ stack: true }), + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }), // This is the format of `vscode.LogOutputChannel` + winston.format.printf(msg => { + return `${msg.timestamp} [${msg.level}] ${msg.message}${msg.stack ? ` ${msg.stack}` : ""}`; + }), + winston.format.colorize() + ), + }); + this.disposables.push( + { + dispose: () => { + this.logger.close(); + if (ouptutChannelTransport.close) { + ouptutChannelTransport.close(); + } + if (rollingLogTransport.close) { + rollingLogTransport.close(); + } + }, + }, + vscode.workspace.onDidChangeConfiguration(e => { + if ( + e.affectsConfiguration("swift.outputChannelLevel") || + e.affectsConfiguration("swift.diagnostics") + ) { + ouptutChannelTransport.level = this.outputChannelLevel; + } + }) + ); + } + + debug(message: LoggerMeta, label?: string, options?: LogMessageOptions) { + this.logger.debug(this.normalizeMessage(message, label), options); + } + + info(message: LoggerMeta, label?: string, options?: LogMessageOptions) { + this.logger.info(this.normalizeMessage(message, label), options); + } + + warn(message: LoggerMeta, label?: string, options?: LogMessageOptions) { + this.logger.warn(this.normalizeMessage(message, label), options); + } + + error(message: LoggerMeta, label?: string, options?: LogMessageOptions) { + if (message instanceof Error) { + this.logger.error(message); + return; + } + this.logger.error(this.normalizeMessage(message, label), options); + } + + get logs(): string[] { + return this.rollingLog.logs; + } + + clear() { + this.outputChannel.clear(); + this.rollingLog.clear(); + } + + private normalizeMessage(message: LoggerMeta, label?: string) { + let fullMessage: string; + if (typeof message === "string") { + fullMessage = message; + } else { + try { + fullMessage = JSON.stringify(message); + } catch (e) { + fullMessage = `${message}`; + } + } + if (label !== undefined) { + fullMessage = `${label}: ${message}`; + } + return fullMessage; + } + + private get outputChannelLevel(): string { + const info = vscode.workspace.getConfiguration("swift").inspect("outputChannelLevel"); + // If the user has explicitly set `outputChannelLevel` then use it, otherwise + // check the deprecated `diagnostics` property + if (info?.globalValue || info?.workspaceValue || info?.workspaceFolderValue) { + return configuration.outputChannelLevel; + } else if (configuration.diagnostics) { + return "debug"; + } else { + return configuration.outputChannelLevel; + } + } + + dispose() { + this.disposables.forEach(d => d.dispose()); + } +} diff --git a/src/logging/SwiftLoggerFactory.ts b/src/logging/SwiftLoggerFactory.ts new file mode 100644 index 000000000..229107e44 --- /dev/null +++ b/src/logging/SwiftLoggerFactory.ts @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import { join } from "path"; +import * as vscode from "vscode"; +import { TemporaryFolder } from "../utilities/tempFolder"; +import { SwiftOutputChannel } from "./SwiftOutputChannel"; +import { SwiftLogger } from "./SwiftLogger"; + +export class SwiftLoggerFactory { + constructor(private logFolderUri: vscode.Uri) {} + + create(name: string, logFilename: string): SwiftLogger; + create(name: string, logFilename: string, options: { outputChannel: true }): SwiftOutputChannel; + create( + name: string, + logFilename: string, + options: { outputChannel: boolean } = { outputChannel: false } + ): SwiftLogger { + return options?.outputChannel + ? new SwiftOutputChannel(name, this.logFilePath(logFilename)) + : new SwiftLogger(name, this.logFilePath(logFilename)); + } + + /** + * This is mainly only intended for testing purposes + */ + async temp(name: string): Promise { + const folder = await TemporaryFolder.create(); + return new SwiftLogger(name, join(folder.path, `${name}.log`)); + } + + private logFilePath(logFilename: string): string { + return join(this.logFolderUri.fsPath, logFilename); + } +} diff --git a/src/logging/SwiftOutputChannel.ts b/src/logging/SwiftOutputChannel.ts new file mode 100644 index 000000000..e0cbc4c63 --- /dev/null +++ b/src/logging/SwiftOutputChannel.ts @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import { SwiftLogger } from "./SwiftLogger"; + +export class SwiftOutputChannel extends SwiftLogger implements vscode.OutputChannel { + /** + * Creates a vscode.OutputChannel that allows for later retrival of logs. + * @param name + */ + constructor(name: string, logFilePath: string, logStoreLinesSize?: number) { + super(name, logFilePath, logStoreLinesSize); + } + + append(value: string): void { + this.info(value, undefined, { append: true }); + } + + appendLine(value: string): void { + this.info(value); + } + + replace(value: string): void { + this.outputChannel.replace(value); + this.rollingLog.replace(value); + } + + show(_column?: unknown, preserveFocus?: boolean | undefined): void { + this.outputChannel.show(preserveFocus); + } + + hide(): void { + this.outputChannel.hide(); + } +} diff --git a/src/sourcekit-lsp/LanguageClientConfiguration.ts b/src/sourcekit-lsp/LanguageClientConfiguration.ts index 9af9ecd66..a4659268b 100644 --- a/src/sourcekit-lsp/LanguageClientConfiguration.ts +++ b/src/sourcekit-lsp/LanguageClientConfiguration.ts @@ -23,7 +23,6 @@ import configuration from "../configuration"; import { Version } from "../utilities/version"; import { WorkspaceContext } from "../WorkspaceContext"; import { DiagnosticsManager } from "../DiagnosticsManager"; -import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; import { promptForDiagnostics } from "../commands/captureDiagnostics"; import { uriConverters } from "./uriConverters"; import { LSPActiveDocumentManager } from "./didChangeActiveDocument"; @@ -167,9 +166,11 @@ export function lspClientOptions( return { documentSelector: LanguagerClientDocumentSelectors.sourcekitLSPDocumentTypes(), revealOutputChannelOn: RevealOutputChannelOn.Never, - workspaceFolder: workspaceFolder, - outputChannel: new SwiftOutputChannel( - `SourceKit Language Server (${swiftVersion.toString()})` + workspaceFolder, + outputChannel: workspaceContext.loggerFactory.create( + `SourceKit Language Server (${swiftVersion.toString()})`, + `sourcekit-lsp-${swiftVersion.toString()}.log`, + { outputChannel: true } ), middleware: { didOpen: activeDocumentManager.didOpen.bind(activeDocumentManager), diff --git a/src/sourcekit-lsp/LanguageClientManager.ts b/src/sourcekit-lsp/LanguageClientManager.ts index d764e0734..dd44aee3b 100644 --- a/src/sourcekit-lsp/LanguageClientManager.ts +++ b/src/sourcekit-lsp/LanguageClientManager.ts @@ -34,13 +34,13 @@ import { FolderContext } from "../FolderContext"; import { Executable, LanguageClient, ServerOptions } from "vscode-languageclient/node"; import { ArgumentFilter, BuildFlags } from "../toolchain/BuildFlags"; import { LSPLogger, LSPOutputChannel } from "./LSPOutputChannel"; -import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; import { activateGetReferenceDocument } from "./getReferenceDocument"; import { LanguageClientFactory } from "./LanguageClientFactory"; import { SourceKitLogMessageNotification, SourceKitLogMessageParams } from "./extensions"; import { LSPActiveDocumentManager } from "./didChangeActiveDocument"; import { DidChangeActiveDocumentNotification } from "./extensions/DidChangeActiveDocumentRequest"; import { lspClientOptions } from "./LanguageClientConfiguration"; +import { SwiftOutputChannel } from "../logging/SwiftOutputChannel"; interface LanguageClientManageOptions { /** @@ -167,9 +167,7 @@ export class LanguageClientManager implements vscode.Disposable { // Swift versions prior to 5.6 don't support file changes, so need to restart // lSP server when a file is either created or deleted if (this.swiftVersion.isLessThan(new Version(5, 6, 0))) { - folderContext.workspaceContext.outputChannel.logDiagnostic( - "LSP: Adding new/delete file handlers" - ); + folderContext.workspaceContext.logger.debug("LSP: Adding new/delete file handlers"); // restart LSP server on creation of a new file const onDidCreateFileDisposable = vscode.workspace.onDidCreateFiles(() => { void this.restart(); @@ -364,7 +362,7 @@ export class LanguageClientManager implements vscode.Disposable { if (reason.message === "Stopping the server timed out") { await this.setupLanguageClient(workspaceFolder); } - this.folderContext.workspaceContext.outputChannel.log(`${reason}`); + this.folderContext.workspaceContext.logger.error(reason); }); await this.restartedPromise; } @@ -448,9 +446,8 @@ export class LanguageClientManager implements vscode.Disposable { folderContext => document.uri.fsPath.startsWith(folderContext.folder.fsPath) ); if (!documentFolderContext) { - this.languageClientOutputChannel?.log( - "Unable to find folder for document: " + document.uri.fsPath, - "WARN" + this.languageClientOutputChannel?.warn( + "Unable to find folder for document: " + document.uri.fsPath ); return; } @@ -487,13 +484,13 @@ export class LanguageClientManager implements vscode.Disposable { }); }); if (client.clientOptions.workspaceFolder) { - this.folderContext.workspaceContext.outputChannel.log( + this.folderContext.workspaceContext.logger.info( `SourceKit-LSP setup for ${FolderContext.uriName( client.clientOptions.workspaceFolder.uri )}` ); } else { - this.folderContext.workspaceContext.outputChannel.log(`SourceKit-LSP setup`); + this.folderContext.workspaceContext.logger.info(`SourceKit-LSP setup`); } client.onNotification(SourceKitLogMessageNotification.type, params => { @@ -533,7 +530,7 @@ export class LanguageClientManager implements vscode.Disposable { } }) .catch(reason => { - this.folderContext.workspaceContext.outputChannel.log(`${reason}`); + this.folderContext.workspaceContext.logger.error(reason); void this.languageClient?.stop(); this.languageClient = undefined; throw reason; diff --git a/src/tasks/TaskManager.ts b/src/tasks/TaskManager.ts index c91e65740..623ccde45 100644 --- a/src/tasks/TaskManager.ts +++ b/src/tasks/TaskManager.ts @@ -104,7 +104,7 @@ export class TaskManager implements vscode.Disposable { }); // setup startingTaskPromise to be resolved one task has started if (this.startingTaskPromise !== undefined) { - this.workspaceContext.outputChannel.appendLine( + this.workspaceContext.logger.error( "TaskManager: Starting promise should be undefined if we reach here." ); } @@ -124,7 +124,7 @@ export class TaskManager implements vscode.Disposable { }); }, error => { - this.workspaceContext.outputChannel.appendLine(`Error executing task: ${error}`); + this.workspaceContext.logger.error(`Error executing task: ${error}`); disposable.dispose(); this.startingTaskPromise = undefined; reject(error); diff --git a/src/tasks/TaskQueue.ts b/src/tasks/TaskQueue.ts index 6b5abca0e..f2861189f 100644 --- a/src/tasks/TaskQueue.ts +++ b/src/tasks/TaskQueue.ts @@ -88,7 +88,7 @@ export class TaskOperation implements SwiftOperation { if (token?.isCancellationRequested) { return Promise.resolve(undefined); } - workspaceContext.outputChannel.log(`Exec Task: ${this.task.detail ?? this.task.name}`); + workspaceContext.logger.info(`Exec Task: ${this.task.detail ?? this.task.name}`); return workspaceContext.tasks.executeTaskAndWait(this.task, token); } } @@ -245,7 +245,7 @@ export class TaskQueue { await this.waitWhileDisabled(); // log start if (operation.log) { - this.workspaceContext.outputChannel.log( + this.workspaceContext.logger.info( `${operation.log}: starting ... `, this.folderContext.name ); @@ -257,13 +257,13 @@ export class TaskQueue { if (operation.log && !operation.token?.isCancellationRequested) { switch (result) { case 0: - this.workspaceContext.outputChannel.log( + this.workspaceContext.logger.info( `${operation.log}: ... done.`, this.folderContext.name ); break; default: - this.workspaceContext.outputChannel.log( + this.workspaceContext.logger.error( `${operation.log}: ... failed.`, this.folderContext.name ); @@ -275,7 +275,7 @@ export class TaskQueue { .catch(error => { // log error if (operation.log) { - this.workspaceContext.outputChannel.log( + this.workspaceContext.logger.error( `${operation.log}: ${error}`, this.folderContext.name ); diff --git a/src/toolchain/SelectedXcodeWatcher.ts b/src/toolchain/SelectedXcodeWatcher.ts index 013c77118..04ccafcbe 100644 --- a/src/toolchain/SelectedXcodeWatcher.ts +++ b/src/toolchain/SelectedXcodeWatcher.ts @@ -14,10 +14,10 @@ import * as fs from "fs/promises"; import * as vscode from "vscode"; -import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; import { showReloadExtensionNotification } from "../ui/ReloadExtension"; import configuration from "../configuration"; import { removeToolchainPath, selectToolchain } from "../ui/ToolchainSelection"; +import { SwiftLogger } from "../logging/SwiftLogger"; export class SelectedXcodeWatcher implements vscode.Disposable { private xcodePath: string | undefined; @@ -30,7 +30,7 @@ export class SelectedXcodeWatcher implements vscode.Disposable { private static XCODE_SYMLINK_LOCATION = "/var/select/developer_dir"; constructor( - private outputChannel: SwiftOutputChannel, + private logger: SwiftLogger, testDependencies?: { checkIntervalMs?: number; xcodeSymlink?: () => Promise; @@ -67,6 +67,7 @@ export class SelectedXcodeWatcher implements vscode.Disposable { */ private async setup() { this.xcodePath = await this.xcodeSymlink(); + this.logger.debug(`Initial Xcode symlink path ${this.xcodePath}`); const developerDir = () => configuration.swiftEnvironmentVariables["DEVELOPER_DIR"]; const matchesPath = (xcodePath: string) => configuration.path && configuration.path.startsWith(xcodePath); @@ -85,7 +86,7 @@ export class SelectedXcodeWatcher implements vscode.Disposable { const newXcodePath = await this.xcodeSymlink(); if (newXcodePath && this.xcodePath !== newXcodePath) { - this.outputChannel.appendLine( + this.logger.info( `Selected Xcode changed from ${this.xcodePath} to ${newXcodePath}` ); this.xcodePath = newXcodePath; diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts index 1e2843cd8..9ccdae720 100644 --- a/src/toolchain/swiftly.ts +++ b/src/toolchain/swiftly.ts @@ -19,6 +19,7 @@ import { execFile, ExecFileError } from "../utilities/utilities"; import * as vscode from "vscode"; import { Version } from "../utilities/version"; import { z } from "zod"; +import { SwiftLogger } from "../logging/SwiftLogger"; const ListResult = z.object({ toolchains: z.array( @@ -57,9 +58,7 @@ export class Swiftly { * @returns the version of Swiftly as a `Version` object, or `undefined` * if Swiftly is not installed or not supported. */ - public static async version( - outputChannel?: vscode.OutputChannel - ): Promise { + public static async version(logger?: SwiftLogger): Promise { if (!Swiftly.isSupported()) { return undefined; } @@ -67,7 +66,7 @@ export class Swiftly { const { stdout } = await execFile("swiftly", ["--version"]); return Version.fromString(stdout.trim()); } catch (error) { - outputChannel?.appendLine(`Failed to retrieve Swiftly version: ${error}`); + logger?.error(`Failed to retrieve Swiftly version: ${error}`); return undefined; } } @@ -77,9 +76,7 @@ export class Swiftly { * * @returns `true` if JSON output is supported, `false` otherwise. */ - private static async supportsJsonOutput( - outputChannel?: vscode.OutputChannel - ): Promise { + private static async supportsJsonOutput(logger?: SwiftLogger): Promise { if (!Swiftly.isSupported()) { return false; } @@ -88,7 +85,7 @@ export class Swiftly { const version = Version.fromString(stdout.trim()); return version?.isGreaterThanOrEqual(new Version(1, 1, 0)) ?? false; } catch (error) { - outputChannel?.appendLine(`Failed to check Swiftly JSON support: ${error}`); + logger?.error(`Failed to check Swiftly JSON support: ${error}`); return false; } } @@ -98,39 +95,35 @@ export class Swiftly { * * @returns an array of toolchain paths */ - public static async listAvailableToolchains( - outputChannel?: vscode.OutputChannel - ): Promise { + public static async listAvailableToolchains(logger?: SwiftLogger): Promise { if (!this.isSupported()) { return []; } - const version = await Swiftly.version(outputChannel); + const version = await Swiftly.version(logger); if (!version) { - outputChannel?.appendLine("Swiftly is not installed"); + logger?.warn("Swiftly is not installed"); return []; } - if (!(await Swiftly.supportsJsonOutput(outputChannel))) { - return await Swiftly.getToolchainInstallLegacy(outputChannel); + if (!(await Swiftly.supportsJsonOutput(logger))) { + return await Swiftly.getToolchainInstallLegacy(logger); } - return await Swiftly.getListAvailableToolchains(outputChannel); + return await Swiftly.getListAvailableToolchains(logger); } - private static async getListAvailableToolchains( - outputChannel?: vscode.OutputChannel - ): Promise { + private static async getListAvailableToolchains(logger?: SwiftLogger): Promise { try { const { stdout } = await execFile("swiftly", ["list", "--format=json"]); const response = ListResult.parse(JSON.parse(stdout)); return response.toolchains.map(t => t.version.name); } catch (error) { - outputChannel?.appendLine(`Failed to retrieve Swiftly installations: ${error}`); + logger?.error(`Failed to retrieve Swiftly installations: ${error}`); return []; } } - private static async getToolchainInstallLegacy(outputChannel?: vscode.OutputChannel) { + private static async getToolchainInstallLegacy(logger?: SwiftLogger) { try { const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; if (!swiftlyHomeDir) { @@ -148,7 +141,7 @@ export class Swiftly { .filter((toolchain): toolchain is string => typeof toolchain === "string") .map(toolchain => path.join(swiftlyHomeDir, "toolchains", toolchain)); } catch (error) { - outputChannel?.appendLine(`Failed to retrieve Swiftly installations: ${error}`); + logger?.error(`Failed to retrieve Swiftly installations: ${error}`); throw new Error( `Failed to retrieve Swiftly installations from disk: ${(error as Error).message}` ); @@ -198,7 +191,7 @@ export class Swiftly { * @returns The location of the active toolchain if swiftly is being used to manage it. */ public static async toolchain( - outputChannel?: vscode.OutputChannel, + logger?: SwiftLogger, cwd?: vscode.Uri ): Promise { const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; @@ -214,7 +207,7 @@ export class Swiftly { return path.join(inUse, "usr"); } } catch (err: unknown) { - outputChannel?.appendLine(`Failed to retrieve Swiftly installations: ${err}`); + logger?.error(`Failed to retrieve Swiftly installations: ${err}`); const error = err as ExecFileError; // Its possible the toolchain in .swift-version is misconfigured or doesn't exist. void vscode.window.showErrorMessage( diff --git a/src/toolchain/toolchain.ts b/src/toolchain/toolchain.ts index 6914d037e..e96dc2ca1 100644 --- a/src/toolchain/toolchain.ts +++ b/src/toolchain/toolchain.ts @@ -18,7 +18,6 @@ import * as os from "os"; import * as plist from "plist"; import * as vscode from "vscode"; import configuration from "../configuration"; -import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; import { execFile, execSwift } from "../utilities/utilities"; import { expandFilePathTilde, fileExists, pathExists } from "../utilities/filesystem"; import { Version } from "../utilities/version"; @@ -26,6 +25,7 @@ import { BuildFlags } from "./BuildFlags"; import { Sanitizer } from "./Sanitizer"; import { lineBreakRegex } from "../utilities/tasks"; import { Swiftly } from "./swiftly"; +import { SwiftLogger } from "../logging/SwiftLogger"; /** * Contents of **Info.plist** on Windows. */ @@ -118,15 +118,12 @@ export class SwiftToolchain { this.swiftVersionString = targetInfo.compilerVersion; } - static async create( - folder?: vscode.Uri, - outputChannel?: vscode.OutputChannel - ): Promise { + static async create(folder?: vscode.Uri, logger?: SwiftLogger): Promise { const { path: swiftFolderPath, isSwiftlyManaged } = await this.getSwiftFolderPath( folder, - outputChannel + logger ); - const toolchainPath = await this.getToolchainPath(swiftFolderPath, folder, outputChannel); + const toolchainPath = await this.getToolchainPath(swiftFolderPath, folder, logger); const targetInfo = await this.getSwiftTargetInfo( this._getToolchainExecutable(toolchainPath, "swift") ); @@ -142,13 +139,15 @@ export class SwiftToolchain { swiftFolderPath, swiftVersion, runtimePath, - customSDK ?? defaultSDK + customSDK ?? defaultSDK, + logger ), this.getSwiftTestingPath( targetInfo, swiftVersion, runtimePath, - customSDK ?? defaultSDK + customSDK ?? defaultSDK, + logger ), this.getSwiftPMTestingHelperPath(toolchainPath), ]); @@ -516,13 +515,13 @@ export class SwiftToolchain { return str; } - logDiagnostics(channel: SwiftOutputChannel) { - channel.logDiagnostic(this.diagnostics); + logDiagnostics(logger: SwiftLogger) { + logger.debug(this.diagnostics); } private static async getSwiftFolderPath( cwd?: vscode.Uri, - outputChannel?: vscode.OutputChannel + logger?: SwiftLogger ): Promise<{ path: string; isSwiftlyManaged: boolean }> { try { let swift: string; @@ -588,7 +587,7 @@ export class SwiftToolchain { isSwiftlyManaged, }; } catch (error) { - outputChannel?.appendLine(`Failed to find swift executable: ${error}`); + logger?.error(`Failed to find swift executable: ${error}`); throw Error("Failed to find swift executable"); } } @@ -624,7 +623,7 @@ export class SwiftToolchain { private static async getToolchainPath( swiftPath: string, cwd?: vscode.Uri, - channel?: vscode.OutputChannel + logger?: SwiftLogger ): Promise { try { switch (process.platform) { @@ -645,7 +644,7 @@ export class SwiftToolchain { return path.dirname(configuration.path); } - const swiftlyToolchainLocation = await Swiftly.toolchain(channel, cwd); + const swiftlyToolchainLocation = await Swiftly.toolchain(logger, cwd); if (swiftlyToolchainLocation) { return swiftlyToolchainLocation; } @@ -749,7 +748,8 @@ export class SwiftToolchain { targetInfo: SwiftTargetInfo, swiftVersion: Version, runtimePath: string | undefined, - sdkroot: string | undefined + sdkroot: string | undefined, + logger?: SwiftLogger ): Promise { if (process.platform !== "win32") { return undefined; @@ -759,7 +759,8 @@ export class SwiftToolchain { targetInfo, swiftVersion, runtimePath, - sdkroot + sdkroot, + logger ); } @@ -775,7 +776,8 @@ export class SwiftToolchain { swiftFolderPath: string, swiftVersion: Version, runtimePath: string | undefined, - sdkroot: string | undefined + sdkroot: string | undefined, + logger?: SwiftLogger ): Promise { switch (process.platform) { case "darwin": { @@ -793,7 +795,8 @@ export class SwiftToolchain { targetInfo, swiftVersion, runtimePath, - sdkroot + sdkroot, + logger ); } } @@ -805,7 +808,8 @@ export class SwiftToolchain { targetInfo: SwiftTargetInfo, swiftVersion: Version, runtimePath: string | undefined, - sdkroot: string | undefined + sdkroot: string | undefined, + logger?: SwiftLogger ): Promise { // look up runtime library directory for XCTest/Testing alternatively const fallbackPath = @@ -838,9 +842,7 @@ export class SwiftToolchain { const plistKey = type === "XCTest" ? "XCTEST_VERSION" : "SWIFT_TESTING_VERSION"; const version = infoPlist.DefaultProperties[plistKey]; if (!version) { - new SwiftOutputChannel("swift").appendLine( - `Warning: ${platformManifest} is missing the ${plistKey} key.` - ); + logger?.warn(`${platformManifest} is missing the ${plistKey} key.`); return undefined; } diff --git a/src/ui/SwiftOutputChannel.ts b/src/ui/SwiftOutputChannel.ts deleted file mode 100644 index 82ae15d46..000000000 --- a/src/ui/SwiftOutputChannel.ts +++ /dev/null @@ -1,165 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VS Code Swift open source project -// -// Copyright (c) 2021 the VS Code Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VS Code Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; -import configuration from "../configuration"; -import { IS_RUNNING_IN_CI } from "../utilities/utilities"; - -export class SwiftOutputChannel implements vscode.OutputChannel { - private channel: vscode.OutputChannel; - private logStore: RollingLog; - - /** - * Creates a vscode.OutputChannel that allows for later retrival of logs. - * @param name - */ - constructor( - public name: string, - logStoreLinesSize: number = 250_000 // default to capturing 250k log lines - ) { - this.name = name; - this.channel = vscode.window.createOutputChannel(name, "Swift"); - this.logStore = new RollingLog(logStoreLinesSize); - } - - append(value: string): void { - this.channel.append(value); - this.logStore.append(value); - } - - appendLine(value: string): void { - this.channel.appendLine(value); - this.logStore.appendLine(value); - } - - replace(value: string): void { - this.channel.replace(value); - this.logStore.replace(value); - } - - clear(): void { - this.channel.clear(); - this.logStore.clear(); - } - - show(_column?: unknown, preserveFocus?: boolean | undefined): void { - this.channel.show(preserveFocus); - } - - hide(): void { - this.channel.hide(); - } - - dispose() { - this.channel.dispose(); - this.logStore.dispose(); - } - - log(message: string, label?: string) { - let fullMessage: string; - if (label !== undefined) { - fullMessage = `${label}: ${message}`; - } else { - fullMessage = message; - } - this.appendLine(`${this.nowFormatted}: ${fullMessage}`); - } - - logDiagnostic(message: string, label?: string) { - if (!configuration.diagnostics && !IS_RUNNING_IN_CI) { - return; - } - const fullMessage = label !== undefined ? `${label}: ${message}` : message; - this.appendLine(`${this.nowFormatted}: ${fullMessage}`); - } - - get nowFormatted(): string { - return new Date().toLocaleString("en-US", { - hourCycle: "h23", - hour: "2-digit", - minute: "numeric", - second: "numeric", - }); - } - - get logs(): string[] { - return this.logStore.logs; - } -} - -class RollingLog implements vscode.Disposable { - private _logs: (string | null)[]; - private startIndex: number = 0; - private endIndex: number = 0; - private logCount: number = 0; - - constructor(private maxLogs: number) { - this._logs = new Array(maxLogs).fill(null); - } - - public get logs(): string[] { - const logs: string[] = []; - for (let i = 0; i < this.logCount; i++) { - logs.push(this._logs[(this.startIndex + i) % this.maxLogs]!); - } - return logs; - } - - private incrementIndex(index: number): number { - return (index + 1) % this.maxLogs; - } - - dispose() { - this.clear(); - } - - clear() { - this._logs = new Array(this.maxLogs).fill(null); - this.startIndex = 0; - this.endIndex = 0; - this.logCount = 0; - } - - appendLine(log: string) { - // Writing to a new line that isn't the very first, increment the end index - if (this.logCount > 0) { - this.endIndex = this.incrementIndex(this.endIndex); - } - - // We're over the window size, move the start index - if (this.logCount === this.maxLogs) { - this.startIndex = this.incrementIndex(this.startIndex); - } else { - this.logCount++; - } - - this._logs[this.endIndex] = log; - } - - append(log: string) { - if (this.logCount === 0) { - this.logCount = 1; - } - const newLogLine = (this._logs[this.endIndex] ?? "") + log; - this._logs[this.endIndex] = newLogLine; - } - - replace(log: string) { - this._logs = new Array(this.maxLogs).fill(null); - this._logs[0] = log; - this.startIndex = 0; - this.endIndex = 1; - this.logCount = 1; - } -} diff --git a/src/ui/ToolchainSelection.ts b/src/ui/ToolchainSelection.ts index 9cff98ebb..d465ee61e 100644 --- a/src/ui/ToolchainSelection.ts +++ b/src/ui/ToolchainSelection.ts @@ -19,6 +19,7 @@ import { SwiftToolchain } from "../toolchain/toolchain"; import configuration from "../configuration"; import { Commands } from "../commands"; import { Swiftly } from "../toolchain/swiftly"; +import { SwiftLogger } from "../logging/SwiftLogger"; /** * Open the installation page on Swift.org @@ -154,6 +155,7 @@ type SelectToolchainItem = SwiftToolchainItem | ActionItem | SeparatorItem; */ async function getQuickPickItems( activeToolchain: SwiftToolchain | undefined, + logger: SwiftLogger, cwd?: vscode.Uri ): Promise { // Find any Xcode installations on the system @@ -201,7 +203,7 @@ async function getQuickPickItems( return result; }); // Find any Swift toolchains installed via Swiftly - const swiftlyToolchains = (await Swiftly.listAvailableToolchains()) + const swiftlyToolchains = (await Swiftly.listAvailableToolchains(logger)) .reverse() .map(toolchainPath => ({ type: "toolchain", @@ -297,11 +299,12 @@ async function getQuickPickItems( */ export async function showToolchainSelectionQuickPick( activeToolchain: SwiftToolchain | undefined, + logger: SwiftLogger, cwd?: vscode.Uri ) { let xcodePaths: string[] = []; const selected = await vscode.window.showQuickPick( - getQuickPickItems(activeToolchain, cwd).then(result => { + getQuickPickItems(activeToolchain, logger, cwd).then(result => { xcodePaths = result .filter((i): i is XcodeToolchainItem => "category" in i && i.category === "xcode") .map(xcode => xcode.xcodePath); diff --git a/src/ui/win32.ts b/src/ui/win32.ts index 2f2760ff4..a93a451ee 100644 --- a/src/ui/win32.ts +++ b/src/ui/win32.ts @@ -13,10 +13,10 @@ //===----------------------------------------------------------------------===// import * as fs from "fs/promises"; -import { SwiftOutputChannel } from "./SwiftOutputChannel"; import { TemporaryFolder } from "../utilities/tempFolder"; import configuration from "../configuration"; import * as vscode from "vscode"; +import { SwiftLogger } from "../logging/SwiftLogger"; /** * Warns the user about lack of symbolic link support on Windows. Performs the @@ -24,9 +24,9 @@ import * as vscode from "vscode"; * * @param outputChannel The Swift output channel to log any errors to */ -export function checkAndWarnAboutWindowsSymlinks(outputChannel: SwiftOutputChannel) { +export function checkAndWarnAboutWindowsSymlinks(logger: SwiftLogger) { if (process.platform === "win32" && configuration.warnAboutSymlinkCreation) { - void isSymlinkAllowed(outputChannel).then(async canCreateSymlink => { + void isSymlinkAllowed(logger).then(async canCreateSymlink => { if (canCreateSymlink) { return; } @@ -53,7 +53,7 @@ export function checkAndWarnAboutWindowsSymlinks(outputChannel: SwiftOutputChann * * @returns whether or not a symlink can be created */ -export async function isSymlinkAllowed(outputChannel?: SwiftOutputChannel): Promise { +export async function isSymlinkAllowed(logger?: SwiftLogger): Promise { const temporaryFolder = await TemporaryFolder.create(); return await temporaryFolder.withTemporaryFile("", async testFilePath => { const testSymlinkPath = temporaryFolder.filename("symlink-"); @@ -62,7 +62,7 @@ export async function isSymlinkAllowed(outputChannel?: SwiftOutputChannel): Prom await fs.unlink(testSymlinkPath); return true; } catch (error) { - outputChannel?.log(`${error}`); + logger?.error(error); return false; } }); diff --git a/src/utilities/utilities.ts b/src/utilities/utilities.ts index d73c7ea71..0f768b40a 100644 --- a/src/utilities/utilities.ts +++ b/src/utilities/utilities.ts @@ -122,7 +122,7 @@ export async function execFile( folderContext?: FolderContext, customSwiftRuntime = true ): Promise<{ stdout: string; stderr: string }> { - folderContext?.workspaceContext.outputChannel.logDiagnostic( + folderContext?.workspaceContext.logger.debug( `Exec: ${executable} ${args.join(" ")}`, folderContext.name ); @@ -158,7 +158,7 @@ export async function execFileStreamOutput( customSwiftRuntime = true, killSignal: NodeJS.Signals = "SIGTERM" ): Promise { - folderContext?.workspaceContext.outputChannel.logDiagnostic( + folderContext?.workspaceContext.logger.debug( `Exec: ${executable} ${args.join(" ")}`, folderContext.name ); diff --git a/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts index 452304716..ab0623abc 100644 --- a/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts +++ b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts @@ -32,7 +32,6 @@ import { import { mutable } from "../../utilities/types"; import { SwiftExecution } from "../../../src/tasks/SwiftExecution"; import { SwiftTask } from "../../../src/tasks/SwiftTaskProvider"; -import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; suite("SwiftPluginTaskProvider Test Suite", function () { let workspaceContext: WorkspaceContext; @@ -43,10 +42,10 @@ suite("SwiftPluginTaskProvider Test Suite", function () { activateExtensionForSuite({ async setup(ctx) { workspaceContext = ctx; - const outputChannel = new SwiftOutputChannel("SwiftPluginTaskProvider.tests"); folderContext = await folderInRootWorkspace("command-plugin", workspaceContext); - await folderContext.loadSwiftPlugins(outputChannel); - expect(outputChannel.logs.length).to.equal(0, `Expected no output channel logs`); + const logger = await ctx.loggerFactory.temp("SwiftPluginTaskProvider.tests"); + await folderContext.loadSwiftPlugins(logger); + expect(logger.logs.length).to.equal(0, `Expected no output channel logs`); expect(workspaceContext.folders).to.not.have.lengthOf(0); }, }); diff --git a/test/integration-tests/testexplorer/XCTestOutputParser.test.ts b/test/integration-tests/testexplorer/XCTestOutputParser.test.ts index 03e472ba7..58bfa893b 100644 --- a/test/integration-tests/testexplorer/XCTestOutputParser.test.ts +++ b/test/integration-tests/testexplorer/XCTestOutputParser.test.ts @@ -23,8 +23,8 @@ import { TestRunState, TestRunTestItem, TestStatus } from "./MockTestRunState"; import { sourceLocationToVSCodeLocation } from "../../../src/utilities/utilities"; import { TestXUnitParser } from "../../../src/TestExplorer/TestXUnitParser"; import { activateExtensionForSuite } from "../utilities/testutilities"; -import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; import { lineBreakRegex } from "../../../src/utilities/tasks"; +import { WorkspaceContext } from "../../../src/WorkspaceContext"; enum ParserTestKind { Regular = "Regular Test Run", @@ -72,8 +72,10 @@ ${tests.map( } let hasMultiLineParallelTestOutput: boolean; + let workspaceContext: WorkspaceContext; activateExtensionForSuite({ async setup(ctx) { + workspaceContext = ctx; hasMultiLineParallelTestOutput = ctx.globalToolchain.hasMultiLineParallelTestOutput; }, }); @@ -86,7 +88,7 @@ ${tests.map( if (parserTestKind === ParserTestKind.Parallel) { const xmlResults = expectedStateToXML(expected); const xmlParser = new TestXUnitParser(hasMultiLineParallelTestOutput); - void xmlParser.parse(xmlResults, testRunState, new SwiftOutputChannel("test")); + void xmlParser.parse(xmlResults, testRunState, workspaceContext.logger); } assert.deepEqual(testRunState.tests, expected); diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts index 6bb2a8281..e7016cd66 100644 --- a/test/integration-tests/ui/ProjectPanelProvider.test.ts +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -34,7 +34,6 @@ import contextKeys from "../../../src/contextKeys"; import { WorkspaceContext } from "../../../src/WorkspaceContext"; import { Version } from "../../../src/utilities/version"; import { wait } from "../../../src/utilities/utilities"; -import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; import { Commands } from "../../../src/commands"; suite("ProjectPanelProvider Test Suite", function () { @@ -49,9 +48,9 @@ suite("ProjectPanelProvider Test Suite", function () { await vscode.workspace.openTextDocument( path.join(folderContext.folder.fsPath, "Package.swift") ); - const outputChannel = new SwiftOutputChannel("ProjectPanelProvider.tests"); - await folderContext.loadSwiftPlugins(outputChannel); - expect(outputChannel.logs.length).to.equal(0, `Expected no output channel logs`); + const logger = await ctx.loggerFactory.temp("ProjectPanelProvider.tests"); + await folderContext.loadSwiftPlugins(logger); + expect(logger.logs.length).to.equal(0, `Expected no output channel logs`); treeProvider = new ProjectPanelProvider(workspaceContext); await workspaceContext.focusFolder(folderContext); const buildAllTask = await createBuildAllTask(folderContext); diff --git a/test/integration-tests/ui/SwiftOutputChannel.test.ts b/test/integration-tests/ui/SwiftOutputChannel.test.ts index 8321b9319..8b9decb28 100644 --- a/test/integration-tests/ui/SwiftOutputChannel.test.ts +++ b/test/integration-tests/ui/SwiftOutputChannel.test.ts @@ -13,14 +13,26 @@ //===----------------------------------------------------------------------===// import * as assert from "assert"; -import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; +import { SwiftOutputChannel } from "../../../src/logging/SwiftOutputChannel"; +import { TemporaryFolder } from "../../../src/utilities/tempFolder"; +import { join } from "path"; suite("SwiftOutputChannel", function () { let channel: SwiftOutputChannel; const channels: SwiftOutputChannel[] = []; + let tempFolder: TemporaryFolder; + + suiteSetup(async function () { + tempFolder = await TemporaryFolder.create(); + }); + setup(function () { const channelName = `SwiftOutputChannel Tests ${this.currentTest?.id ?? ""}`; - channel = new SwiftOutputChannel(channelName, 3); + channel = new SwiftOutputChannel( + channelName, + join(tempFolder.path, "SwiftOutputChannel.test.log"), + 3 + ); channels.push(channel); }); diff --git a/test/integration-tests/utilities/testutilities.ts b/test/integration-tests/utilities/testutilities.ts index 761ae7c5c..b40dacbb8 100644 --- a/test/integration-tests/utilities/testutilities.ts +++ b/test/integration-tests/utilities/testutilities.ts @@ -23,9 +23,9 @@ import { waitForNoRunningTasks } from "../../utilities/tasks"; import { closeAllEditors } from "../../utilities/commands"; import { isDeepStrictEqual } from "util"; import { Version } from "../../../src/utilities/version"; -import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; import configuration from "../../../src/configuration"; import { buildAllTaskName, resetBuildAllTaskCache } from "../../../src/tasks/SwiftTaskProvider"; +import { SwiftLogger } from "../../../src/logging/SwiftLogger"; export function getRootWorkspaceFolder(): vscode.WorkspaceFolder { const result = vscode.workspace.workspaceFolders?.at(0); @@ -33,9 +33,9 @@ export function getRootWorkspaceFolder(): vscode.WorkspaceFolder { return result; } -function printLogs(outputChannel: SwiftOutputChannel, message: string) { +function printLogs(logger: SwiftLogger, message: string) { console.error(`${message}, captured logs are:`); - outputChannel.logs.map(log => console.log(log)); + logger.logs.map(log => console.log(log)); console.log("======== END OF LOGS ========\n\n"); } @@ -116,7 +116,7 @@ const extensionBootstrapper = (() => { // Mocha will throw an error to break out of a test if `.skip` is used. if (error.message?.indexOf("sync skip;") === -1) { console.error(`Error during test/suite setup, captured logs are:`); - workspaceContext.outputChannel.logs.map(log => console.error(log)); + workspaceContext.logger.logs.map(log => console.error(log)); console.log("======== END OF LOGS ========\n\n"); } throw error; @@ -125,19 +125,14 @@ const extensionBootstrapper = (() => { mocha.beforeEach(function () { if (this.currentTest && activatedAPI) { - activatedAPI.outputChannel.clear(); - activatedAPI.outputChannel.appendLine( - `Starting test: ${testTitle(this.currentTest)}` - ); + activatedAPI.logger.clear(); + activatedAPI.logger.info(`Starting test: ${testTitle(this.currentTest)}`); } }); mocha.afterEach(async function () { if (this.currentTest && activatedAPI && this.currentTest.isFailed()) { - printLogs( - activatedAPI.outputChannel, - `Test failed: ${testTitle(this.currentTest)}` - ); + printLogs(activatedAPI.logger, `Test failed: ${testTitle(this.currentTest)}`); } if (vscode.debug.activeDebugSession) { await vscode.debug.stopDebugging(vscode.debug.activeDebugSession); @@ -156,7 +151,7 @@ const extensionBootstrapper = (() => { } } catch (error) { if (workspaceContext) { - printLogs(workspaceContext.outputChannel, "Error during test/suite teardown"); + printLogs(workspaceContext.logger, "Error during test/suite teardown"); } // We always want to restore settings and deactivate the extension even if the // user supplied teardown fails. That way we have the best chance at not causing @@ -223,7 +218,7 @@ const extensionBootstrapper = (() => { if (!workspaceContext) { printLogs( - activatedAPI.outputChannel, + activatedAPI.logger, "Error during test/suite setup, workspace context could not be created" ); throw new Error("Extension did not activate. Workspace context is not available."); diff --git a/test/unit-tests/commands/captureDiagnostics.test.ts b/test/unit-tests/commands/captureDiagnostics.test.ts index 493a7236a..e84b2d4b7 100644 --- a/test/unit-tests/commands/captureDiagnostics.test.ts +++ b/test/unit-tests/commands/captureDiagnostics.test.ts @@ -23,12 +23,12 @@ import { captureDiagnostics } from "../../../src/commands/captureDiagnostics"; import { WorkspaceContext } from "../../../src/WorkspaceContext"; import { FolderContext } from "../../../src/FolderContext"; import { Version } from "../../../src/utilities/version"; -import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; import { SwiftToolchain } from "../../../src/toolchain/toolchain"; +import { SwiftLogger } from "../../../src/logging/SwiftLogger"; suite("captureDiagnostics Test Suite", () => { let mockContext: MockedObject; - let mockedOutputChannel: MockedObject; + let mockedLogger: MockedObject; let mockedToolchain: MockedObject; const mockWindow = mockGlobalObject(vscode, "window"); @@ -41,14 +41,14 @@ suite("captureDiagnostics Test Suite", () => { folder: vscode.Uri.file("/folder1"), toolchain: instance(mockedToolchain), }); - mockedOutputChannel = mockObject({ - log: mockFn(), + mockedLogger = mockObject({ + info: mockFn(), logs: ["hello", "world"], }); mockContext = mockObject({ folders: [instance(mockedFolder)], globalToolchainSwiftVersion: new Version(6, 0, 0), - outputChannel: instance(mockedOutputChannel), + logger: instance(mockedLogger), }); mockWindow.showInformationMessage.resolves("Minimal" as any); }); @@ -85,7 +85,7 @@ suite("captureDiagnostics Test Suite", () => { mockContext = mockObject({ folders: [instance(mockedFolder1), instance(mockedFolder2)], globalToolchainSwiftVersion: new Version(6, 0, 0), - outputChannel: instance(mockedOutputChannel), + logger: instance(mockedLogger), }); mockWindow.showInformationMessage.resolves("Minimal" as any); }); diff --git a/test/unit-tests/debugger/debugAdapterFactory.test.ts b/test/unit-tests/debugger/debugAdapterFactory.test.ts index ade452fe4..5265bde8e 100644 --- a/test/unit-tests/debugger/debugAdapterFactory.test.ts +++ b/test/unit-tests/debugger/debugAdapterFactory.test.ts @@ -28,29 +28,29 @@ import * as mockFS from "mock-fs"; import { LaunchConfigType, SWIFT_LAUNCH_CONFIG_TYPE } from "../../../src/debugger/debugAdapter"; import * as lldb from "../../../src/debugger/lldb"; import { SwiftToolchain } from "../../../src/toolchain/toolchain"; -import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; import * as debugAdapter from "../../../src/debugger/debugAdapter"; import { Result } from "../../../src/utilities/result"; import configuration from "../../../src/configuration"; import { WorkspaceContext } from "../../../src/WorkspaceContext"; import { FolderContext } from "../../../src/FolderContext"; +import { SwiftLogger } from "../../../src/logging/SwiftLogger"; suite("LLDBDebugConfigurationProvider Tests", () => { let mockWorkspaceContext: MockedObject; let mockToolchain: MockedObject; - let mockOutputChannel: MockedObject; + let mockLogger: MockedObject; const mockDebugAdapter = mockGlobalObject(debugAdapter, "DebugAdapter"); const mockWindow = mockGlobalObject(vscode, "window"); setup(() => { mockToolchain = mockObject({ swiftVersion: new Version(6, 0, 0) }); - mockOutputChannel = mockObject({ - log: mockFn(), + mockLogger = mockObject({ + info: mockFn(), }); mockWorkspaceContext = mockObject({ globalToolchain: instance(mockToolchain), globalToolchainSwiftVersion: new Version(6, 0, 0), - outputChannel: instance(mockOutputChannel), + logger: instance(mockLogger), subscriptions: [], folders: [], }); @@ -60,7 +60,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( undefined, @@ -78,7 +78,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( undefined, @@ -99,7 +99,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( undefined, @@ -120,7 +120,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( undefined, @@ -162,7 +162,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -180,7 +180,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); await expect( configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -202,7 +202,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); await expect( configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -225,7 +225,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); await expect( configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -260,7 +260,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -280,7 +280,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); await expect( configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -297,7 +297,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "win32", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -315,7 +315,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "win32", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -333,7 +333,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -351,7 +351,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "linux", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -369,7 +369,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -391,7 +391,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -407,7 +407,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -429,7 +429,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -468,7 +468,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", instance(mockWorkspaceContext), - instance(mockOutputChannel) + instance(mockLogger) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( { diff --git a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts index 85151a22c..09b0b15ce 100644 --- a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts +++ b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts @@ -20,7 +20,6 @@ import { FolderEvent, FolderOperation, WorkspaceContext } from "../../../src/Wor import { Version } from "../../../src/utilities/version"; import { SwiftToolchain } from "../../../src/toolchain/toolchain"; import { BuildFlags } from "../../../src/toolchain/BuildFlags"; -import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; import { MockedObject, mockObject, @@ -50,6 +49,7 @@ import { DidChangeActiveDocumentNotification, DidChangeActiveDocumentParams, } from "../../../src/sourcekit-lsp/extensions/DidChangeActiveDocumentRequest"; +import { SwiftLogger } from "../../../src/logging/SwiftLogger"; suite("LanguageClientManager Suite", () => { let languageClientFactoryMock: MockedObject; @@ -59,7 +59,7 @@ suite("LanguageClientManager Suite", () => { let mockedWorkspace: MockedObject; let mockedFolder: MockedObject; let didChangeFoldersEmitter: AsyncEventEmitter; - let mockedOutputChannel: MockedObject; + let mockLogger: MockedObject; let mockedToolchain: MockedObject; let mockedBuildFlags: MockedObject; @@ -109,10 +109,9 @@ suite("LanguageClientManager Suite", () => { s.withArgs("sourcekit-lsp").returns("/path/to/toolchain/bin/sourcekit-lsp") ), }); - mockedOutputChannel = mockObject({ - log: s => s, - logDiagnostic: s => s, - appendLine: () => {}, + mockLogger = mockObject({ + info: s => s, + debug: s => s, }); didChangeFoldersEmitter = new AsyncEventEmitter(); mockedFolder = mockObject({ @@ -127,7 +126,7 @@ suite("LanguageClientManager Suite", () => { mockObject({ globalToolchain: instance(mockedToolchain), globalToolchainSwiftVersion: new Version(6, 0, 0), - outputChannel: instance(mockedOutputChannel), + logger: instance(mockLogger), }) ), swiftVersion: new Version(6, 0, 0), @@ -136,7 +135,7 @@ suite("LanguageClientManager Suite", () => { mockedWorkspace = mockObject({ globalToolchain: instance(mockedToolchain), globalToolchainSwiftVersion: new Version(6, 0, 0), - outputChannel: instance(mockedOutputChannel), + logger: instance(mockLogger), subscriptions: [], folders: [instance(mockedFolder)], onDidChangeFolders: mockFn(s => s.callsFake(didChangeFoldersEmitter.event)), diff --git a/test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts b/test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts index fc2912d0d..e6a60d35d 100644 --- a/test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts +++ b/test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts @@ -15,7 +15,6 @@ import * as vscode from "vscode"; import { expect } from "chai"; import { SelectedXcodeWatcher } from "../../../src/toolchain/SelectedXcodeWatcher"; -import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; import { instance, MockedObject, @@ -26,10 +25,11 @@ import { } from "../../MockUtils"; import configuration from "../../../src/configuration"; import { Commands } from "../../../src/commands"; +import { SwiftLogger } from "../../../src/logging/SwiftLogger"; suite("Selected Xcode Watcher", () => { const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); - let mockOutputChannel: MockedObject; + let mockLogger: MockedObject; const pathConfig = mockGlobalValue(configuration, "path"); const envConfig = mockGlobalValue(configuration, "swiftEnvironmentVariables"); const mockWorkspace = mockGlobalObject(vscode, "workspace"); @@ -42,8 +42,9 @@ suite("Selected Xcode Watcher", () => { this.skip(); } - mockOutputChannel = mockObject({ - appendLine: mockFn(), + mockLogger = mockObject({ + debug: mockFn(), + info: mockFn(), }); pathConfig.setValue(""); @@ -58,7 +59,7 @@ suite("Selected Xcode Watcher", () => { async function run(symLinksOnCallback: (string | undefined)[]) { return new Promise(resolve => { let ctr = 0; - const watcher = new SelectedXcodeWatcher(instance(mockOutputChannel), { + const watcher = new SelectedXcodeWatcher(instance(mockLogger), { checkIntervalMs: 1, xcodeSymlink: async () => { if (ctr >= symLinksOnCallback.length) { From b369df4d3dd644168900fc86119ea8fa21ef3011 Mon Sep 17 00:00:00 2001 From: Adam Ward Date: Mon, 28 Jul 2025 15:28:50 -0400 Subject: [PATCH 2/7] Fix transpiled errors that not a constructor --- package-lock.json | 3 ++- package.json | 3 ++- src/logging/OutputChannelTransport.ts | 6 ++++- src/logging/RollingLog.ts | 18 -------------- src/logging/RollingLogTransport.ts | 36 +++++++++++++++++++++++++++ src/logging/SwiftLogger.ts | 2 +- 6 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 src/logging/RollingLogTransport.ts diff --git a/package-lock.json b/package-lock.json index 0b1ddae44..805aa1bc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,7 +72,8 @@ "svgo": "^4.0.0", "tsx": "^4.20.3", "typescript": "^5.8.3", - "winston": "^3.17.0" + "winston": "^3.17.0", + "winston-transport": "^4.9.0" }, "engines": { "vscode": "^1.88.0" diff --git a/package.json b/package.json index 886408c05..0d470c7e0 100644 --- a/package.json +++ b/package.json @@ -1886,7 +1886,8 @@ "svgo": "^4.0.0", "tsx": "^4.20.3", "typescript": "^5.8.3", - "winston": "^3.17.0" + "winston": "^3.17.0", + "winston-transport": "^4.9.0" }, "dependencies": { "@vscode/codicons": "^0.0.38", diff --git a/src/logging/OutputChannelTransport.ts b/src/logging/OutputChannelTransport.ts index c785fcb6e..eaa88d2d8 100644 --- a/src/logging/OutputChannelTransport.ts +++ b/src/logging/OutputChannelTransport.ts @@ -12,7 +12,11 @@ // //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import * as Transport from "winston-transport"; +import * as TransportType from "winston-transport"; + +// Compile error if don't use "require": https://github.com/swiftlang/vscode-swift/actions/runs/16529946578/job/46752753379?pr=1746 +// eslint-disable-next-line @typescript-eslint/no-require-imports +const Transport: typeof TransportType = require("winston-transport"); export class OutputChannelTransport extends Transport { private appending: boolean = false; diff --git a/src/logging/RollingLog.ts b/src/logging/RollingLog.ts index 279b4aa10..f168137c4 100644 --- a/src/logging/RollingLog.ts +++ b/src/logging/RollingLog.ts @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import * as Transport from "winston-transport"; export class RollingLog implements vscode.Disposable { private _logs: (string | null)[]; @@ -79,20 +78,3 @@ export class RollingLog implements vscode.Disposable { this.logCount = 1; } } - -export class RollingLogTransport extends Transport { - constructor(private readonly rollingLog: RollingLog) { - super(); - this.level = "info"; // This log is used for testing, we - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public log(info: any, next: () => void): void { - if (info.append) { - this.rollingLog.append(info.message); - } else { - this.rollingLog.appendLine(info.message); - } - next(); - } -} diff --git a/src/logging/RollingLogTransport.ts b/src/logging/RollingLogTransport.ts new file mode 100644 index 000000000..547812649 --- /dev/null +++ b/src/logging/RollingLogTransport.ts @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as TransportType from "winston-transport"; +import { RollingLog } from "./RollingLog"; + +// Compile error if don't use "require": https://github.com/swiftlang/vscode-swift/actions/runs/16529946578/job/46752753379?pr=1746 +// eslint-disable-next-line @typescript-eslint/no-require-imports +const Transport: typeof TransportType = require("winston-transport"); + +export class RollingLogTransport extends Transport { + constructor(private rollingLog: RollingLog) { + super(); + this.level = "info"; // This log is used for testing, we + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public log(info: any, next: () => void): void { + if (info.append) { + this.rollingLog.append(info.message); + } else { + this.rollingLog.appendLine(info.message); + } + next(); + } +} diff --git a/src/logging/SwiftLogger.ts b/src/logging/SwiftLogger.ts index 69812804a..48d6612ba 100644 --- a/src/logging/SwiftLogger.ts +++ b/src/logging/SwiftLogger.ts @@ -15,7 +15,7 @@ import * as vscode from "vscode"; import * as winston from "winston"; import { RollingLog } from "./RollingLog"; -import { RollingLogTransport } from "./RollingLog"; +import { RollingLogTransport } from "./RollingLogTransport"; import { IS_RUNNING_UNDER_TEST } from "../utilities/utilities"; import { OutputChannelTransport } from "./OutputChannelTransport"; import configuration from "../configuration"; From d2be0f7b3d4e40ac2c032485a475fc77d89151aa Mon Sep 17 00:00:00 2001 From: Adam Ward Date: Tue, 29 Jul 2025 09:11:54 -0400 Subject: [PATCH 3/7] Fix failing tests --- .../commands/captureDiagnostics.test.ts | 78 ++++++------------- .../LanguageClientManager.test.ts | 10 ++- 2 files changed, 34 insertions(+), 54 deletions(-) rename test/{unit-tests => integration-tests}/commands/captureDiagnostics.test.ts (50%) diff --git a/test/unit-tests/commands/captureDiagnostics.test.ts b/test/integration-tests/commands/captureDiagnostics.test.ts similarity index 50% rename from test/unit-tests/commands/captureDiagnostics.test.ts rename to test/integration-tests/commands/captureDiagnostics.test.ts index e84b2d4b7..7d25728ef 100644 --- a/test/unit-tests/commands/captureDiagnostics.test.ts +++ b/test/integration-tests/commands/captureDiagnostics.test.ts @@ -15,97 +15,69 @@ import * as vscode from "vscode"; import * as path from "path"; import * as os from "os"; -import * as fs from "fs/promises"; +import { mkdir, rm } from "fs/promises"; import * as decompress from "decompress"; import { expect } from "chai"; -import { instance, MockedObject, mockFn, mockGlobalObject, mockObject } from "../../MockUtils"; import { captureDiagnostics } from "../../../src/commands/captureDiagnostics"; import { WorkspaceContext } from "../../../src/WorkspaceContext"; -import { FolderContext } from "../../../src/FolderContext"; -import { Version } from "../../../src/utilities/version"; -import { SwiftToolchain } from "../../../src/toolchain/toolchain"; -import { SwiftLogger } from "../../../src/logging/SwiftLogger"; +import { mockGlobalObject } from "../../MockUtils"; +import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; suite("captureDiagnostics Test Suite", () => { - let mockContext: MockedObject; - let mockedLogger: MockedObject; - let mockedToolchain: MockedObject; + let workspaceContext: WorkspaceContext; const mockWindow = mockGlobalObject(vscode, "window"); + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + }, + testAssets: ["defaultPackage"], + }); + setup(() => { - mockedToolchain = mockObject({ - swiftVersion: new Version(6, 0, 0), - diagnostics: "some diagnostics", - }); - const mockedFolder = mockObject({ - folder: vscode.Uri.file("/folder1"), - toolchain: instance(mockedToolchain), - }); - mockedLogger = mockObject({ - info: mockFn(), - logs: ["hello", "world"], - }); - mockContext = mockObject({ - folders: [instance(mockedFolder)], - globalToolchainSwiftVersion: new Version(6, 0, 0), - logger: instance(mockedLogger), - }); mockWindow.showInformationMessage.resolves("Minimal" as any); }); test("Should capture dianostics to a zip file", async () => { - const zipPath = await captureDiagnostics(instance(mockContext)); + const zipPath = await captureDiagnostics(workspaceContext); expect(zipPath).to.not.be.undefined; }); test("Should validate a single folder project zip file has contents", async () => { - const zipPath = await captureDiagnostics(instance(mockContext)); + const zipPath = await captureDiagnostics(workspaceContext); expect(zipPath).to.not.be.undefined; const { files, folder } = await decompressZip(zipPath as string); validate( files.map(file => file.path), - ["extension-logs.txt", "folder1-[a-z0-9]+-settings.txt"] + ["swift-vscode-extension.log", "defaultPackage-[a-z0-9]+-settings.txt"] ); - await fs.rm(folder, { recursive: true, force: true }); + await rm(folder, { recursive: true, force: true }); }); suite("Multiple folder project", () => { - setup(() => { - const mockedFolder1 = mockObject({ - folder: vscode.Uri.file("/folder1"), - toolchain: instance(mockedToolchain), - }); - const mockedFolder2 = mockObject({ - folder: vscode.Uri.file("/folder2"), - toolchain: instance(mockedToolchain), - }); - mockContext = mockObject({ - folders: [instance(mockedFolder1), instance(mockedFolder2)], - globalToolchainSwiftVersion: new Version(6, 0, 0), - logger: instance(mockedLogger), - }); - mockWindow.showInformationMessage.resolves("Minimal" as any); + setup(async () => { + await folderInRootWorkspace("dependencies", workspaceContext); }); test("Should validate a multiple folder project zip file has contents", async () => { - const zipPath = await captureDiagnostics(instance(mockContext)); + const zipPath = await captureDiagnostics(workspaceContext); expect(zipPath).to.not.be.undefined; const { files, folder } = await decompressZip(zipPath as string); validate( files.map(file => file.path), [ - "extension-logs.txt", - "folder1/", - "folder1/folder1-[a-z0-9]+-settings.txt", - "folder2/", - "folder2/folder2-[a-z0-9]+-settings.txt", + "swift-vscode-extension.log", + "defaultPackage/", + "defaultPackage/defaultPackage-[a-z0-9]+-settings.txt", + "dependencies/", + "dependencies/dependencies-[a-z0-9]+-settings.txt", ] ); - await fs.rm(folder, { recursive: true, force: true }); + await rm(folder, { recursive: true, force: true }); }); }); @@ -116,7 +88,7 @@ suite("captureDiagnostics Test Suite", () => { os.tmpdir(), `vscode-swift-test-${Math.random().toString(36).substring(7)}` ); - await fs.mkdir(tempDir, { recursive: true }); + await mkdir(tempDir, { recursive: true }); return { folder: tempDir, files: await decompress(zipPath as string, tempDir) }; } diff --git a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts index 09b0b15ce..407e55066 100644 --- a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts +++ b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts @@ -50,6 +50,8 @@ import { DidChangeActiveDocumentParams, } from "../../../src/sourcekit-lsp/extensions/DidChangeActiveDocumentRequest"; import { SwiftLogger } from "../../../src/logging/SwiftLogger"; +import { SwiftOutputChannel } from "../../../src/logging/SwiftOutputChannel"; +import { SwiftLoggerFactory } from "../../../src/logging/SwiftLoggerFactory"; suite("LanguageClientManager Suite", () => { let languageClientFactoryMock: MockedObject; @@ -60,6 +62,7 @@ suite("LanguageClientManager Suite", () => { let mockedFolder: MockedObject; let didChangeFoldersEmitter: AsyncEventEmitter; let mockLogger: MockedObject; + let mockLoggerFactory: MockedObject; let mockedToolchain: MockedObject; let mockedBuildFlags: MockedObject; @@ -113,6 +116,9 @@ suite("LanguageClientManager Suite", () => { info: s => s, debug: s => s, }); + mockLoggerFactory = mockObject({ + create: mockFn(s => s.returns(mockObject({}))), + }); didChangeFoldersEmitter = new AsyncEventEmitter(); mockedFolder = mockObject({ isRootFolder: false, @@ -127,6 +133,7 @@ suite("LanguageClientManager Suite", () => { globalToolchain: instance(mockedToolchain), globalToolchainSwiftVersion: new Version(6, 0, 0), logger: instance(mockLogger), + loggerFactory: instance(mockLoggerFactory), }) ), swiftVersion: new Version(6, 0, 0), @@ -136,6 +143,7 @@ suite("LanguageClientManager Suite", () => { globalToolchain: instance(mockedToolchain), globalToolchainSwiftVersion: new Version(6, 0, 0), logger: instance(mockLogger), + loggerFactory: instance(mockLoggerFactory), subscriptions: [], folders: [instance(mockedFolder)], onDidChangeFolders: mockFn(s => s.callsFake(didChangeFoldersEmitter.event)), @@ -150,7 +158,7 @@ suite("LanguageClientManager Suite", () => { code2ProtocolConverter: instance(mockedConverter), clientOptions: {}, outputChannel: instance( - mockObject({ + mockObject({ dispose: mockFn(), }) ), From 1ee22f716cff011288122daa592155a4923482f6 Mon Sep 17 00:00:00 2001 From: award999 Date: Thu, 31 Jul 2025 09:50:19 -0400 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: Rishi --- package.json | 2 +- src/logging/SwiftLogger.ts | 2 +- src/logging/SwiftLoggerFactory.ts | 2 +- src/logging/SwiftOutputChannel.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0d470c7e0..c80c2b249 100644 --- a/package.json +++ b/package.json @@ -588,7 +588,7 @@ "swift.outputChannelLevel": { "type": "string", "default": "info", - "markdownDescription": "The the level of the Swift output channel. This has no affect on the verbosity of messages written to the extension's log file.", + "markdownDescription": "The log level of the Swift output channel. This has no effect on the verbosity of messages written to the extension's log file.", "enum": [ "debug", "info", diff --git a/src/logging/SwiftLogger.ts b/src/logging/SwiftLogger.ts index 48d6612ba..4ac1549e9 100644 --- a/src/logging/SwiftLogger.ts +++ b/src/logging/SwiftLogger.ts @@ -2,7 +2,7 @@ // // This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VS Code Swift project authors +// Copyright (c) 2025 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/src/logging/SwiftLoggerFactory.ts b/src/logging/SwiftLoggerFactory.ts index 229107e44..a7541337b 100644 --- a/src/logging/SwiftLoggerFactory.ts +++ b/src/logging/SwiftLoggerFactory.ts @@ -2,7 +2,7 @@ // // This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VS Code Swift project authors +// Copyright (c) 2025 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/src/logging/SwiftOutputChannel.ts b/src/logging/SwiftOutputChannel.ts index e0cbc4c63..52fe143b8 100644 --- a/src/logging/SwiftOutputChannel.ts +++ b/src/logging/SwiftOutputChannel.ts @@ -2,7 +2,7 @@ // // This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VS Code Swift project authors +// Copyright (c) 2025 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -17,7 +17,7 @@ import { SwiftLogger } from "./SwiftLogger"; export class SwiftOutputChannel extends SwiftLogger implements vscode.OutputChannel { /** - * Creates a vscode.OutputChannel that allows for later retrival of logs. + * Creates a vscode.OutputChannel that allows for later retrieval of logs. * @param name */ constructor(name: string, logFilePath: string, logStoreLinesSize?: number) { From b86c2a0a23ca663cd64dbe6805d5fd761bd929eb Mon Sep 17 00:00:00 2001 From: Adam Ward Date: Thu, 31 Jul 2025 09:51:49 -0400 Subject: [PATCH 5/7] Fix up comment --- src/logging/RollingLogTransport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logging/RollingLogTransport.ts b/src/logging/RollingLogTransport.ts index 547812649..520803e72 100644 --- a/src/logging/RollingLogTransport.ts +++ b/src/logging/RollingLogTransport.ts @@ -21,7 +21,7 @@ const Transport: typeof TransportType = require("winston-transport"); export class RollingLogTransport extends Transport { constructor(private rollingLog: RollingLog) { super(); - this.level = "info"; // This log is used for testing, we + this.level = "info"; // This log is used for testing, we don't want to hold verbose log messages } // eslint-disable-next-line @typescript-eslint/no-explicit-any From f81e2b08dac74e15821c36f1a9b94ceef3b89e7e Mon Sep 17 00:00:00 2001 From: Adam Ward Date: Thu, 31 Jul 2025 11:45:37 -0400 Subject: [PATCH 6/7] Rename setting to `outputChannelLogLevel` --- package.json | 4 ++-- src/configuration.ts | 4 ++-- src/logging/SwiftLogger.ts | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index c80c2b249..4884de8e2 100644 --- a/package.json +++ b/package.json @@ -585,7 +585,7 @@ "markdownDescription": "The path to a directory that will be used to store attachments produced during a test run.\n\nA relative path resolves relative to the root directory of the workspace running the test(s)", "scope": "machine-overridable" }, - "swift.outputChannelLevel": { + "swift.outputChannelLogLevel": { "type": "string", "default": "info", "markdownDescription": "The log level of the Swift output channel. This has no effect on the verbosity of messages written to the extension's log file.", @@ -946,7 +946,7 @@ "type": "boolean", "default": false, "markdownDescription": "Output additional diagnostics to the Swift output channel.", - "deprecationMessage": "**Deprecated**: Please use `#swift.outputChannelLevel#` instead.", + "deprecationMessage": "**Deprecated**: Please use `#swift.outputChannelLogLevel#` instead.", "order": 100, "scope": "machine-overridable" } diff --git a/src/configuration.ts b/src/configuration.ts index 774d831c7..db85a2adf 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -522,8 +522,8 @@ const configuration = { /* Put in worker queue */ }); }, - get outputChannelLevel(): string { - return vscode.workspace.getConfiguration("swift").get("outputChannelLevel", "info"); + get outputChannelLogLevel(): string { + return vscode.workspace.getConfiguration("swift").get("outputChannelLogLevel", "info"); }, }; diff --git a/src/logging/SwiftLogger.ts b/src/logging/SwiftLogger.ts index 4ac1549e9..4a1f79c6a 100644 --- a/src/logging/SwiftLogger.ts +++ b/src/logging/SwiftLogger.ts @@ -75,7 +75,7 @@ export class SwiftLogger implements vscode.Disposable { }, vscode.workspace.onDidChangeConfiguration(e => { if ( - e.affectsConfiguration("swift.outputChannelLevel") || + e.affectsConfiguration("swift.outputChannelLogLevel") || e.affectsConfiguration("swift.diagnostics") ) { ouptutChannelTransport.level = this.outputChannelLevel; @@ -131,15 +131,15 @@ export class SwiftLogger implements vscode.Disposable { } private get outputChannelLevel(): string { - const info = vscode.workspace.getConfiguration("swift").inspect("outputChannelLevel"); - // If the user has explicitly set `outputChannelLevel` then use it, otherwise + const info = vscode.workspace.getConfiguration("swift").inspect("outputChannelLogLevel"); + // If the user has explicitly set `outputChannelLogLevel` then use it, otherwise // check the deprecated `diagnostics` property if (info?.globalValue || info?.workspaceValue || info?.workspaceFolderValue) { - return configuration.outputChannelLevel; + return configuration.outputChannelLogLevel; } else if (configuration.diagnostics) { return "debug"; } else { - return configuration.outputChannelLevel; + return configuration.outputChannelLogLevel; } } From 0ef38f59f2376e425de5b4e0ade398f5ed33b76d Mon Sep 17 00:00:00 2001 From: Adam Ward Date: Thu, 31 Jul 2025 13:06:00 -0400 Subject: [PATCH 7/7] Add changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b908ef1b..ce6e342c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ ### Added - New `swift.createTasksForLibraryProducts` setting that when enabled causes the extension to automatically create and provide tasks for library products ([#1741](https://github.com/swiftlang/vscode-swift/pull/1741)) +- New `swift.outputChannelLogLevel` setting to control the verbosity of the `Swift` output channel ([#1746](https://github.com/swiftlang/vscode-swift/pull/1746)) + +### Changed +- Added log levels and improved Swift extension logging so a logfile is produced in addition to logging messages to the existing `Swift` output channel. Deprecated the `swift.diagnostics` setting in favour of the new `swift.outputChannelLogLevel` setting ([#1746](https://github.com/swiftlang/vscode-swift/pull/1746)) ## 2.10.0 - 2025-07-28