diff --git a/.travis.yml b/.travis.yml index 1bc82d6..ac5c535 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ os: - osx language: node_js node_js: - - 6.5.0 - - 7 + - 12 + - 14 before_install: - "npm i -g typescript" - "npm i -g vsce" diff --git a/package.json b/package.json index 49d4830..4246c0e 100644 --- a/package.json +++ b/package.json @@ -36,27 +36,75 @@ "activationEvents": [ "onLanguage:swift", "workspaceContains:**/*swift", - "onCommand:sde.commands.buildPackage" + "onCommand:sde.commands.build", + "onCommand:sde.commands.run", + "onCommand:sde.commands.clean", + "onCommand:sde.commands.selectRun" ], - "main": "./out/src/clientMain", + "main": "./out/clientMain", "contributes": { "commands": [ { - "command": "sde.commands.buildPackage", + "command": "sde.commands.build", "title": "Build Package", "category": "SDE" + }, + { + "command": "sde.commands.restartLanguageServer", + "title": "Restart Language Server", + "category": "SDE" + }, + { + "command": "sde.commands.run", + "title": "Run Default Target", + "category": "SDE", + "enablement": "sde:running == false" + }, + { + "command": "sde.commands.selectRun", + "title": "Run Target…", + "category": "SDE", + "enablement": "sde:running == false" + }, + { + "command": "sde.commands.restartRun", + "title": "Restart Target", + "category": "SDE", + "enablement": "sde:running == true" + }, + { + "command": "sde.commands.stop", + "title": "Stop Running Target", + "category": "SDE", + "enablement": "sde:running == true" + }, + { + "command": "sde.commands.clean", + "title": "Clean Package", + "category": "SDE" } ], "keybindings": [ { - "command": "sde.commands.buildPackage", - "key": "alt+b", - "mac": "alt+b" + "command": "sde.commands.build", + "key": "alt+b" + }, + { + "command": "sde.commands.run", + "key": "alt+r" + }, + { + "command": "sde.commands.selectRun", + "key": "alt+shift+r" + }, + { + "command": "sde.commands.stop", + "key": "alt+s" } ], "configuration": { "type": "object", - "title": "Swift Development Environment Configuration", + "title": "Swift Development Environment", "properties": { "sourcekit-lsp.serverPath": { "type": "string", @@ -140,7 +188,7 @@ "sde.enable": { "type": "boolean", "default": true, - "description": "Wether SDE shall be executed." + "description": "Enable SDE functionality" }, "sde.languageServerMode": { "type": "string", @@ -200,33 +248,33 @@ { "language": "swift" } - ], - "taskDefinitions": [ - { - "type": "swift", - "properties": {} - } ] }, + "prettier": { + "trailingComma": "es5", + "tabWidth": 2, + "printWidth": 100 + }, "scripts": { "vscode:prepublish": "npm run build", "build": "npm run compile", - "compile": "tsc -p ./", - "format": "prettier CHANGELOG.md README.md src/*.ts src/server/**/*.ts tsconfig.json --write", - "test": "jest" + "compile": "npx tsc", + "format": "npx prettier CHANGELOG.md README.md src/*.ts src/server/**/*.ts tsconfig.json --write", + "test": "npx jest" }, "devDependencies": { "@types/bunyan": "^1.8.4", "@types/glob": "^5.0.35", "@types/jest": "^24.0.18", "@types/js-yaml": "^3.11.1", - "@types/node": "^12.7.4", + "@types/node": "^12.19.7", "@types/vscode": "1.30.0", "@types/xml-js": "^1.0.0", "jest": "^24.9.0", "prettier": "^1.18.2", "ts-jest": "^24.0.2", - "typescript": "^3.6.2" + "tsc": "^1.20150623.0", + "typescript": "^4.1.2" }, "dependencies": { "bunyan": "^1.8.5", diff --git a/src/SwiftTools.ts b/src/SwiftTools.ts deleted file mode 100644 index d659a0d..0000000 --- a/src/SwiftTools.ts +++ /dev/null @@ -1,142 +0,0 @@ -"use strict"; - -import { - Uri, - Diagnostic, - DiagnosticSeverity, - Range, - window as vscodeWindow -} from "vscode"; -import cp = require("child_process"); -import { - trace, - dumpInConsole, - diagnosticCollection, - makeBuildStatusFailed, - makeBuildStatusSuccessful -} from "./clientMain"; - -let stdout: string; -///managed build now only support to invoke on save -export function buildPackage( - swiftBinPath: string, - pkgPath: string, - params: string[] -) { - stdout = ""; - const sb = cp.spawn(swiftBinPath, params, { cwd: pkgPath, shell: true }); - sb.stdout.on("data", data => { - stdout += data; - dumpInConsole("" + data); - }); - sb.stderr.on("data", data => { - dumpInConsole("" + data); - }); - sb.on("error", function(err: Error) { - trace("***swift build command error*** " + err.message); - if (err.message.indexOf("ENOENT") > 0) { - const msg = - "The '" + - swiftBinPath + - "' command is not available." + - " Please check your swift executable user setting and ensure it is installed."; - vscodeWindow.showErrorMessage(msg); - } - }); - - sb.on("exit", function(code, signal) { - trace(`***swift build command exited*** code: ${code}, signal: ${signal}`); - dumpInConsole("\n"); - diagnosticCollection.clear(); - dumpDiagnostics(); - - if (code != 0) { - makeBuildStatusFailed(); - } else { - makeBuildStatusSuccessful(); - } - }); -} - -function dumpDiagnostics() { - const diagnosticMap: Map = new Map(); - let diags: Array = []; - const lines = stdout.split("\n"); - - function isDiagStartLine(line: string) { - //FIXME - const sa = line.split(":"); - if (sa.length > 4) { - const sev = sa[3].trim(); - return sev == "error" || sev == "warning" || sev == "note"; - } - return false; - } - //FIXME always the pattern? - function makeDiagnostic(oneDiag: string[]) { - const line0 = oneDiag[0]; - const line1 = oneDiag[1]; - const line2 = oneDiag[2]; - const sa = line0.split(":"); - const file = Uri.file(sa[0]).toString(); //FIXME not always file, Swift._cos:1:13: - //line and column in vscode is 0-based - const line = Number(sa[1]) - 1; - const startColumn: number = Number(sa[2]) - 1; - const sev = toVSCodeSeverity(sa[3].trim()); - const msg = sa[4]; - const endColumn: number = startColumn + line2.trim().length; - - // let canonicalFile = vscode.Uri.file(error.file).toString(); - // if (document && document.uri.toString() === canonicalFile) { - // let range = new vscode.Range(error.line - 1, 0, error.line - 1, document.lineAt(error.line - 1).range.end.character + 1); - // let text = document.getText(range); - // let [_, leading, trailing] = /^(\s*).*(\s*)$/.exec(text); - // startColumn = leading.length; - // endColumn = text.length - trailing.length; - // } - let range = new Range(line, startColumn, line, endColumn); - let diagnostic = new Diagnostic(range, msg, sev); - let diagnostics = diagnosticMap.get(file); - if (!diagnostics) { - diagnostics = []; - } - diagnostics.push(diagnostic); - diagnosticMap.set(file, diagnostics); - } - - let index = Number.MAX_VALUE; - let line, oneDiag, hasDiagStart; - for (let i = 0; i < lines.length; i++) { - line = lines[i]; - if (isDiagStartLine(line)) { - if (!hasDiagStart) hasDiagStart = true; - if (oneDiag) diags.push(oneDiag); - oneDiag = []; - } - if (hasDiagStart) { - oneDiag.push(line); - } - } - diags.push(oneDiag); //push last oneDiag - diags.forEach(d => { - if (d) { - makeDiagnostic(d); - } - }); - diagnosticMap.forEach((diags, file) => { - diagnosticCollection.set(Uri.parse(file), diags); - }); -} - -function toVSCodeSeverity(sev: string) { - switch (sev) { - case "error": - return DiagnosticSeverity.Error; - case "warning": - return DiagnosticSeverity.Warning; - case "note": - return DiagnosticSeverity.Information; - default: - return DiagnosticSeverity.Error; //FIXME - } -} diff --git a/src/clientMain.ts b/src/clientMain.ts index 84e4faa..5d4ec14 100644 --- a/src/clientMain.ts +++ b/src/clientMain.ts @@ -1,357 +1,110 @@ "use strict"; -import * as path from "path"; import * as fs from "fs"; -import * as tools from "./SwiftTools"; -import { - workspace, - window, - commands, - languages, - ExtensionContext, - DiagnosticCollection, - StatusBarItem, - StatusBarAlignment, - OutputChannel, - debug -} from "vscode"; -import { - LanguageClient, - LanguageClientOptions, - ServerOptions, - TransportKind, - Executable -} from "vscode-languageclient"; -import { absolutePath } from "./AbsolutePath"; -import { promisify } from "util"; +import * as path from "path"; +import { commands, DiagnosticCollection, ExtensionContext, window, workspace } from "vscode"; +import { absolutePath } from "./helpers/AbsolutePath"; +import * as tools from "./toolchain/SwiftTools"; +import * as config from "./vscode/config-helpers"; +import lsp from "./vscode/lsp-interop"; +import output from "./vscode/output-channels"; +import { statusBarItem } from "./vscode/status-bar"; let swiftBinPath: string | null = null; let swiftBuildParams: string[] = ["build"]; -let swiftPackageManifestPath: string | null = null; let skProtocolProcess: string | null = null; let skProtocolProcessAsShellCmd: string | null = null; -export let isTracingOn: boolean = false; -export let isLSPServerTracingOn: boolean = false; export let diagnosticCollection: DiagnosticCollection; -let spmChannel: OutputChannel = null; - -function shouldBuildOnSave(): boolean { - const should = workspace.getConfiguration().get("sde.buildOnSave"); - if (should === undefined) { - return true; - } else { - return should; - } -} - -async function currentServerOptions( - context: ExtensionContext -): Promise { - function sourcekiteServerOptions() { - // The server is implemented in node - const serverModule = context.asAbsolutePath( - path.join("out/src/server", "server.js") - ); - // The debug options for the server - const debugOptions = { - execArgv: ["--nolazy", "--inspect=6004"], - ...process.env - }; - - // If the extension is launched in debug mode then the debug server options are used - // Otherwise the run options are used - const serverOptions: ServerOptions = { - run: { - module: serverModule, - transport: TransportKind.ipc, - options: debugOptions - }, - debug: { - module: serverModule, - transport: TransportKind.ipc, - options: debugOptions - } - }; - return serverOptions; - } - - function lspServerOptions() { - // Load the path to the language server from settings - const executableCommand = workspace - .getConfiguration("swift") - .get("languageServerPath", "/usr/local/bin/LanguageServer"); - - const run: Executable = { - command: executableCommand, - options: process.env - }; - const debug: Executable = run; - const serverOptions: ServerOptions = { - run: run, - debug: debug - }; - return serverOptions; - } - - async function sourcekitLspServerOptions() { - const toolchain = workspace - .getConfiguration("sourcekit-lsp") - .get("toolchainPath"); - - async function sourceKitLSPLocation() { - const explicit = workspace - .getConfiguration("sourcekit-lsp") - .get("serverPath", null); - if (explicit) return explicit; - - const sourcekitLSPPath = path.resolve( - toolchain || - "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain", - "usr/bin/sourcekit-lsp" - ); - const isPreinstalled = await promisify(fs.exists)(sourcekitLSPPath); - if (isPreinstalled) { - return sourcekitLSPPath; - } - return workspace - .getConfiguration("swift") - .get("languageServerPath", "/usr/local/bin/sourcekit-lsp"); - } - - // sourcekit-lsp takes -Xswiftc arguments like "swift build", but it doesn't need "build" argument - let sourceKitArgs = ( - workspace.getConfiguration().get("sde.swiftBuildingParams") || - [] - ).filter(param => param !== "build"); - - const env: NodeJS.ProcessEnv = toolchain - ? { ...process.env, SOURCEKIT_TOOLCHAIN_PATH: toolchain } - : process.env; - - const run: Executable = { - command: await sourceKitLSPLocation(), - options: { env }, - args: sourceKitArgs - }; - const serverOptions: ServerOptions = run; - return serverOptions; - } - - const lspMode = workspace - .getConfiguration("sde") - .get("languageServerMode", "sourcekit-lsp"); - - if (lspMode === "sourcekit-lsp") { - return sourcekitLspServerOptions(); - } else if (lspMode === "langserver") { - return lspServerOptions(); - } else { - return sourcekiteServerOptions(); - } -} +let mostRecentRunTarget = ""; -function currentClientOptions( - _context: ExtensionContext -): Partial { - const lspMode = workspace.getConfiguration("sde").get("languageServerMode"); - if (lspMode === "sourcekit-lsp") { - return { - documentSelector: ["swift", "cpp", "c", "objective-c", "objective-cpp"], - synchronize: undefined - }; - } else { - return {}; - } -} +export function activate(context: ExtensionContext) { + output.init(context); -export async function activate(context: ExtensionContext) { if (workspace.getConfiguration().get("sde.enable") === false) { + output.build.log("SDE Disabled", false); return; } - initConfig(); + tools.setRunning(false); + output.build.log("Activating SDE"); - // Options to control the language client - let clientOptions: LanguageClientOptions = { - // Register the server for plain text documents - documentSelector: [ - { language: "swift", scheme: "file" }, - { pattern: "*.swift", scheme: "file" } - ], - synchronize: { - configurationSection: ["swift", "editor", "[swift]"], - // Notify the server about file changes to '.clientrc files contain in the workspace - fileEvents: [ - workspace.createFileSystemWatcher("**/*.swift"), - workspace.createFileSystemWatcher(".build/*.yaml") - ] - }, - initializationOptions: { - isLSPServerTracingOn: isLSPServerTracingOn, - skProtocolProcess: skProtocolProcess, - skProtocolProcessAsShellCmd: skProtocolProcessAsShellCmd, - skCompilerOptions: workspace - .getConfiguration() - .get("sde.sourcekit.compilerOptions"), - toolchainPath: - workspace - .getConfiguration("sourcekit-lsp") - .get("toolchainPath") || null - }, - ...currentClientOptions(context) - }; + initConfig(); + lsp.startLSPClient(context); - // Create the language client and start the client. - const langClient = new LanguageClient( - "Swift", - await currentServerOptions(context), - clientOptions + //commands + let toolchain = new tools.Toolchain( + swiftBinPath, + workspace.workspaceFolders[0].uri.fsPath, + swiftBuildParams + ); + context.subscriptions.push(toolchain.diagnostics); + context.subscriptions.push(toolchain.start()); + context.subscriptions.push( + commands.registerCommand("sde.commands.build", () => toolchain.build()), + commands.registerCommand("sde.commands.restartLanguageServer", () => + lsp.restartLSPClient(context) + ), + commands.registerCommand("sde.commands.run", () => toolchain.runStart()), + commands.registerCommand("sde.commands.selectRun", () => { + window + .showInputBox({ prompt: "Run which target?", value: mostRecentRunTarget }) + .then(target => { + if (!target) { + return; + } + mostRecentRunTarget = target; + toolchain.runStart(target); + }); + }), + commands.registerCommand("sde.commands.restart", () => { + toolchain.runStop(); + toolchain.runStart(mostRecentRunTarget); + }), + commands.registerCommand("sde.commands.stop", () => toolchain.runStop()), + commands.registerCommand("sde.commands.clean", () => toolchain.clean()) ); - let disposable = langClient.start(); - context.subscriptions.push(disposable); - diagnosticCollection = languages.createDiagnosticCollection("swift"); - context.subscriptions.push(diagnosticCollection); - function buildSPMPackage() { - if (isSPMProject()) { - //setup - if (!buildStatusItem) { - initBuildStatusItem(); + workspace.onDidSaveTextDocument( + document => { + if (tools.shouldBuildOnSave() && document.languageId === "swift") { + toolchain.build(); } - - makeBuildStatusStarted(); - tools.buildPackage(swiftBinPath, workspace.rootPath, swiftBuildParams); - } - } - //commands - context.subscriptions.push( - commands.registerCommand("sde.commands.buildPackage", buildSPMPackage) + }, + null, + context.subscriptions ); - if (shouldBuildOnSave()) { - // build on save - workspace.onDidSaveTextDocument( - document => { - if (document.languageId === "swift") { - buildSPMPackage(); - } - }, - null, - context.subscriptions - ); - } + // respond to settings changes + workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration("sde")) { + // reload things as necessary + } + }); // build on startup - buildSPMPackage(); + toolchain.build(); } function initConfig() { checkToolsAvailability(); - - isTracingOn = ( - workspace.getConfiguration().get("sde.enableTracing.client") - ); - isLSPServerTracingOn = ( - workspace.getConfiguration().get("sde.enableTracing.LSPServer") - ); - //FIXME rootPath may be undefined for adhoc file editing mode??? - swiftPackageManifestPath = path.join(workspace.rootPath, "Package.swift"); - - spmChannel = window.createOutputChannel("SPM"); -} - -export let buildStatusItem: StatusBarItem; -let originalBuildStatusItemColor = null; -function initBuildStatusItem() { - buildStatusItem = window.createStatusBarItem(StatusBarAlignment.Left); - originalBuildStatusItemColor = buildStatusItem.color; -} - -const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; -let building: NodeJS.Timer | null = null; - -function makeBuildStatusStarted() { - buildStatusItem.color = originalBuildStatusItemColor; - buildStatusItem.show(); - let animation = frame(); - if (building) { - clearInterval(building); - } - building = setInterval(() => { - buildStatusItem.text = `${animation()} building`; - }, 100); -} - -function frame() { - var i = 0; - return function() { - return frames[(i = ++i % frames.length)]; - }; -} - -export function makeBuildStatusFailed() { - clearInterval(building); - buildStatusItem.text = "$(issue-opened) build failed"; - buildStatusItem.color = "red"; -} - -export function makeBuildStatusSuccessful() { - clearInterval(building); - buildStatusItem.text = "$(check) build succeeded"; - buildStatusItem.color = originalBuildStatusItemColor; -} - -function isSPMProject(): boolean { - return fs.existsSync(swiftPackageManifestPath); } -export function trace(...msg: any[]) { - if (isTracingOn) { - console.log(...msg); - } -} - -export function dumpInConsole(msg: string) { - spmChannel.append(msg); -} - -// function getSkProtocolProcessPath(extPath: string) { -// switch (os.platform()) { -// case 'darwin': -// return path.join(extPath, "bin", "macos", 'sourcekitd-repl') -// default://FIXME -// return path.join(extPath, "bin", "linux", 'sourcekitd-repl') -// } -// } - function checkToolsAvailability() { - swiftBinPath = absolutePath( - workspace.getConfiguration().get("swift.path.swift_driver_bin") - ); - swiftBuildParams = ( - workspace.getConfiguration().get("sde.swiftBuildingParams") - ) || ["build"]; - const sourcekitePath = absolutePath( - workspace.getConfiguration().get("swift.path.sourcekite") - ); + swiftBinPath = absolutePath(workspace.getConfiguration().get("swift.path.swift_driver_bin")); + swiftBuildParams = workspace.getConfiguration().get("sde.swiftBuildingParams") || [ + "build", + ]; + const sourcekitePath = absolutePath(workspace.getConfiguration().get("swift.path.sourcekite")); const sourcekitePathEnableShCmd = workspace .getConfiguration() .get("swift.path.sourcekiteDockerMode"); - const shellPath = absolutePath( - workspace.getConfiguration().get("swift.path.shell") - ); - // const useBuiltInBin = workspace.getConfiguration().get('swift.sourcekit.use_built_in_bin') - // if (useBuiltInBin) { - // skProtocolProcess = getSkProtocolProcessPath( - // extensions.getExtension(PUBLISHER_NAME).extensionPath) - // } else { + const shellPath = absolutePath(workspace.getConfiguration().get("swift.path.shell")); skProtocolProcess = sourcekitePath; skProtocolProcessAsShellCmd = sourcekitePathEnableShCmd; - // } if (!swiftBinPath || !fs.existsSync(swiftBinPath)) { window.showErrorMessage( - 'missing dependent swift tool, please configure correct "swift.path.swift_driver_bin"' + 'missing dependent `swift` tool, please configure correct "swift.path.swift_driver_bin"' ); } if (!sourcekitePathEnableShCmd) { diff --git a/src/AbsolutePath.ts b/src/helpers/AbsolutePath.ts similarity index 100% rename from src/AbsolutePath.ts rename to src/helpers/AbsolutePath.ts diff --git a/src/server/current.ts b/src/sourcekites-server/current.ts similarity index 100% rename from src/server/current.ts rename to src/sourcekites-server/current.ts diff --git a/src/server/package.ts b/src/sourcekites-server/package.ts similarity index 100% rename from src/server/package.ts rename to src/sourcekites-server/package.ts diff --git a/src/server/packages/available-packages.ts b/src/sourcekites-server/packages/available-packages.ts similarity index 93% rename from src/server/packages/available-packages.ts rename to src/sourcekites-server/packages/available-packages.ts index b1fc773..2088e52 100644 --- a/src/server/packages/available-packages.ts +++ b/src/sourcekites-server/packages/available-packages.ts @@ -10,12 +10,12 @@ export const availablePackages: Package = async fromPath => { configTargets, debugYamlTargets, descriptionTargets, - swiftFileTargets + swiftFileTargets, ] = await Promise.all([ configPackage(fromPath), debugYamlPackage(fromPath), descriptionPackage(fromPath), - swiftFilePackage(fromPath) + swiftFilePackage(fromPath), ]); return flatteningTargetsWithUniqueSources( configTargets, diff --git a/src/server/packages/config-package.ts b/src/sourcekites-server/packages/config-package.ts similarity index 73% rename from src/server/packages/config-package.ts rename to src/sourcekites-server/packages/config-package.ts index 6f2cd38..86e6ee5 100644 --- a/src/server/packages/config-package.ts +++ b/src/sourcekites-server/packages/config-package.ts @@ -8,13 +8,10 @@ export const configPackage: Package = async fromPath => { const targets = Current.config.targets .filter( ({ path: targetPath }) => - path.isAbsolute(targetPath) || - fs.existsSync(path.resolve(fromPath, targetPath)) + path.isAbsolute(targetPath) || fs.existsSync(path.resolve(fromPath, targetPath)) ) .map(async configTarget => { - const targetPath = path.normalize( - path.resolve(fromPath, configTarget.path) - ); + const targetPath = path.normalize(path.resolve(fromPath, configTarget.path)); const expandedSources = (configTarget.sources || ["**/*.swift"]).map( expandingSourceGlob(fromPath, targetPath) ); @@ -23,7 +20,7 @@ export const configPackage: Package = async fromPath => { ...configTarget, path: targetPath, sources: new Set([].concat(...sources)), - compilerArguments: configTarget.compilerArguments || [] + compilerArguments: configTarget.compilerArguments || [], }; }); return await Promise.all(targets); diff --git a/src/server/packages/debug-yaml-package.ts b/src/sourcekites-server/packages/debug-yaml-package.ts similarity index 83% rename from src/server/packages/debug-yaml-package.ts rename to src/sourcekites-server/packages/debug-yaml-package.ts index 77d94f0..44ebcc3 100644 --- a/src/server/packages/debug-yaml-package.ts +++ b/src/sourcekites-server/packages/debug-yaml-package.ts @@ -41,11 +41,9 @@ export const debugYamlPackage: Package = async fromPath => { name: command["module-name"] || name, path: fromPath, // actually a subfolder, but all paths are absolute sources: new Set( - command.sources.map(toSource => - path.normalize(path.resolve(fromPath, toSource)) - ) + command.sources.map(toSource => path.normalize(path.resolve(fromPath, toSource))) ), - compilerArguments: compilerArgumentsForCommand(command) + compilerArguments: compilerArgumentsForCommand(command), }); } return targets; @@ -67,18 +65,13 @@ function compilerArgumentsForCommand(command: LLCommand): string[] { const importPaths = command["import-paths"] || []; const otherArgs = command["other-args"] || []; const moduleNameArgs = - (command["module-name"] && [ - "-module-name", - command["module-name"], - "-Onone" - ]) || - []; + (command["module-name"] && ["-module-name", command["module-name"], "-Onone"]) || []; const importPathArgs = importPaths.map(compilerArgumentsForImportPath); return otherArgs.concat(moduleNameArgs, ...importPathArgs); } function contentsOfDebugOrReleaseYaml(fromPath: Path) { - return contentsOfFile(path.resolve(fromPath, ".build", "debug.yaml")).catch( - () => contentsOfFile(path.resolve(fromPath, ".build", "release.yaml")) + return contentsOfFile(path.resolve(fromPath, ".build", "debug.yaml")).catch(() => + contentsOfFile(path.resolve(fromPath, ".build", "release.yaml")) ); } diff --git a/src/server/packages/description-package.ts b/src/sourcekites-server/packages/description-package.ts similarity index 87% rename from src/server/packages/description-package.ts rename to src/sourcekites-server/packages/description-package.ts index dbdf49e..a0803a3 100644 --- a/src/server/packages/description-package.ts +++ b/src/sourcekites-server/packages/description-package.ts @@ -16,8 +16,7 @@ export const descriptionPackage: Package = async fromPath => { try { const data = await Current.swift(fromPath, `package describe --type json`); const packageDescription = JSON.parse(data) as PackageDescription; - const targetDescription = - packageDescription.modules || packageDescription.targets; + const targetDescription = packageDescription.modules || packageDescription.targets; return targetDescription.map(targetFromDescriptionFromPath(fromPath)); } catch (error) { Current.report(error); @@ -34,8 +33,8 @@ function targetFromDescriptionFromPath(fromPath: Path) { compilerArguments: [ "-I", joinPath(fromPath, ".build", "debug"), - ...Current.defaultCompilerArguments() - ] + ...Current.defaultCompilerArguments(), + ], }; }; } diff --git a/src/server/packages/package-helpers.spec.ts b/src/sourcekites-server/packages/package-helpers.spec.ts similarity index 77% rename from src/server/packages/package-helpers.spec.ts rename to src/sourcekites-server/packages/package-helpers.spec.ts index fa81e8f..9ddc47e 100644 --- a/src/server/packages/package-helpers.spec.ts +++ b/src/sourcekites-server/packages/package-helpers.spec.ts @@ -1,21 +1,18 @@ -import { - removingDuplicateSources, - flatteningTargetsWithUniqueSources -} from "./package-helpers"; +import { removingDuplicateSources, flatteningTargetsWithUniqueSources } from "./package-helpers"; import { Target } from "../package"; const uniqueTarget: Target = { name: "Unique", path: "Sources/Unique", sources: new Set(["Hello.swift", "main.swift"]), - compilerArguments: [] + compilerArguments: [], }; const unrelatedTarget: Target = { name: "UnrelatedTarget", path: "Sources/UnrelatedTarget", sources: new Set(["Unrelated.swift"]), - compilerArguments: [] + compilerArguments: [], }; describe("package helpers", () => { @@ -26,17 +23,14 @@ describe("package helpers", () => { }); it("unrelated source sets will be kept", () => { - const emittedTargets = removingDuplicateSources( - [unrelatedTarget], - [uniqueTarget] - ); + const emittedTargets = removingDuplicateSources([unrelatedTarget], [uniqueTarget]); expect(emittedTargets).toEqual([unrelatedTarget]); }); it("unrelated source sets with differing paths will be kept for same file names", () => { const unrelatedTargetWithSameFileNames: Target = { ...unrelatedTarget, - sources: uniqueTarget.sources + sources: uniqueTarget.sources, }; const emittedTargets = removingDuplicateSources( @@ -49,7 +43,7 @@ describe("package helpers", () => { it("source sets with same paths but different file names are kept", () => { const samePathTargetWithDifferentSources = { ...unrelatedTarget, - path: uniqueTarget.path + path: uniqueTarget.path, }; const emittedTargets = removingDuplicateSources( [samePathTargetWithDifferentSources], @@ -66,7 +60,7 @@ describe("package helpers", () => { Array(uniqueTarget.sources.values()).map( sourceFile => `${uniqueTarget.path}/${sourceFile}` ) - ) + ), }; const emittedTargets = removingDuplicateSources( [differentPathTargetWithSameSources], @@ -85,28 +79,24 @@ describe("package helpers", () => { name: "HiModuleFromConfigs", path: "/Users/vknabel/Desktop/AutocompleteIos/Sources/Hi", sources: new Set(["Hi.swift"]), - compilerArguments: [] - } + compilerArguments: [], + }, ], [ { name: "HiModuleFromDebugYaml", path: "/Users/vknabel/Desktop/AutocompleteIos", - sources: new Set([ - "/Users/vknabel/Desktop/AutocompleteIos/Sources/Hi/Hi.swift" - ]), - compilerArguments: [] - } + sources: new Set(["/Users/vknabel/Desktop/AutocompleteIos/Sources/Hi/Hi.swift"]), + compilerArguments: [], + }, ], [ { name: "AutocompleteIos", path: "/Users/vknabel/Desktop/AutocompleteIos", - sources: new Set([ - "/Users/vknabel/Desktop/AutocompleteIos/Package.swift" - ]), - compilerArguments: [] - } + sources: new Set(["/Users/vknabel/Desktop/AutocompleteIos/Package.swift"]), + compilerArguments: [], + }, ] ); expect(emittedTargets).toEqual([ @@ -114,22 +104,20 @@ describe("package helpers", () => { name: "HiModuleFromConfigs", path: "/Users/vknabel/Desktop/AutocompleteIos/Sources/Hi", sources: new Set(["Hi.swift"]), - compilerArguments: [] + compilerArguments: [], }, { name: "HiModuleFromDebugYaml", path: "/Users/vknabel/Desktop/AutocompleteIos", sources: new Set([]), - compilerArguments: [] + compilerArguments: [], }, { name: "AutocompleteIos", path: "/Users/vknabel/Desktop/AutocompleteIos", - sources: new Set([ - "/Users/vknabel/Desktop/AutocompleteIos/Package.swift" - ]), - compilerArguments: [] - } + sources: new Set(["/Users/vknabel/Desktop/AutocompleteIos/Package.swift"]), + compilerArguments: [], + }, ]); }); }); diff --git a/src/server/packages/package-helpers.ts b/src/sourcekites-server/packages/package-helpers.ts similarity index 68% rename from src/server/packages/package-helpers.ts rename to src/sourcekites-server/packages/package-helpers.ts index 6aa2d73..7425905 100644 --- a/src/server/packages/package-helpers.ts +++ b/src/sourcekites-server/packages/package-helpers.ts @@ -1,28 +1,21 @@ import { Target } from "../package"; import * as path from "path"; -export function flatteningTargetsWithUniqueSources( - ...targets: Target[][] -): Target[] { +export function flatteningTargetsWithUniqueSources(...targets: Target[][]): Target[] { return targets.reduce( (current, next) => [ ...current, - ...removingDuplicateSources(next, current.map(normalizedTarget)) + ...removingDuplicateSources(next, current.map(normalizedTarget)), ], [] ); } -export function removingDuplicateSources( - fromTargets: Target[], - uniqueTargets: Target[] -): Target[] { +export function removingDuplicateSources(fromTargets: Target[], uniqueTargets: Target[]): Target[] { return fromTargets.map(target => { const swiftFilesWithoutTargets = Array.from(target.sources).filter( source => - uniqueTargets.findIndex(desc => - desc.sources.has(path.resolve(target.path, source)) - ) === -1 + uniqueTargets.findIndex(desc => desc.sources.has(path.resolve(target.path, source))) === -1 ); return { ...target, sources: new Set(swiftFilesWithoutTargets) }; }); @@ -31,7 +24,7 @@ export function removingDuplicateSources( function normalizedTarget(target: Target): Target { return { ...target, - sources: mapSet(target.sources, source => path.resolve(target.path, source)) + sources: mapSet(target.sources, source => path.resolve(target.path, source)), }; } function mapSet(set: Set, transform: (element: T) => R): Set { diff --git a/src/server/packages/swift-file-package.ts b/src/sourcekites-server/packages/swift-file-package.ts similarity index 71% rename from src/server/packages/swift-file-package.ts rename to src/sourcekites-server/packages/swift-file-package.ts index 62492a2..b2cb854 100644 --- a/src/server/packages/swift-file-package.ts +++ b/src/sourcekites-server/packages/swift-file-package.ts @@ -8,21 +8,17 @@ export const swiftFilePackage: Package = async fromPath => { name: path.basename(fromPath), path: fromPath, sources: new Set( - allSwiftFilesInPath(fromPath).map(file => - path.normalize(path.resolve(fromPath, file)) - ) + allSwiftFilesInPath(fromPath).map(file => path.normalize(path.resolve(fromPath, file))) ), - compilerArguments: [] - } + compilerArguments: [], + }, ]; }; function allSwiftFilesInPath(root: Path): Path[] { const result = new Array(); try { - const dir = fs - .readdirSync(root) - .filter(sub => !sub.startsWith(".") && sub !== "Carthage"); + const dir = fs.readdirSync(root).filter(sub => !sub.startsWith(".") && sub !== "Carthage"); for (const sub of dir) { if (path.extname(sub) === ".swift") { result.push(path.join(root, sub)); diff --git a/src/server/path-helpers.ts b/src/sourcekites-server/path-helpers.ts similarity index 100% rename from src/server/path-helpers.ts rename to src/sourcekites-server/path-helpers.ts diff --git a/src/server/server.ts b/src/sourcekites-server/server.ts similarity index 100% rename from src/server/server.ts rename to src/sourcekites-server/server.ts diff --git a/src/server/sourcekit-xml.ts b/src/sourcekites-server/sourcekit-xml.ts similarity index 100% rename from src/server/sourcekit-xml.ts rename to src/sourcekites-server/sourcekit-xml.ts diff --git a/src/server/sourcekites.ts b/src/sourcekites-server/sourcekites.ts similarity index 100% rename from src/server/sourcekites.ts rename to src/sourcekites-server/sourcekites.ts diff --git a/src/server/thenable.d.ts b/src/sourcekites-server/thenable.d.ts similarity index 100% rename from src/server/thenable.d.ts rename to src/sourcekites-server/thenable.d.ts diff --git a/src/toolchain/SwiftTools.ts b/src/toolchain/SwiftTools.ts new file mode 100644 index 0000000..dd0b667 --- /dev/null +++ b/src/toolchain/SwiftTools.ts @@ -0,0 +1,234 @@ +"use strict"; + +import * as cp from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import { Duplex, Readable, Stream } from "stream"; +import { + commands, + Diagnostic, + DiagnosticCollection, + DiagnosticSeverity, + Disposable, + languages, + Range, + Uri, + workspace, +} from "vscode"; +import * as config from "../vscode/config-helpers"; +import output, { LogStream } from "../vscode/output-channels"; +import { statusBarItem } from "../vscode/status-bar"; + +type OnProcExit = (code: number, signal: NodeJS.Signals) => void; +type ChildProc = cp.ChildProcessWithoutNullStreams; +type ProcAndOutput = { proc: ChildProc; output: Promise }; + +const DiagnosticFirstLine = /(.+?):(\d+):(\d+): (error|warning|note|.+?): (.+)/; + +export function setRunning(isRunning: boolean) { + commands.executeCommand("setContext", "sde:running", isRunning); +} + +export function swiftPackageExists(): boolean { + const manifestPath = workspace.workspaceFolders + ? path.join(workspace.workspaceFolders[0].uri.fsPath, "Package.swift") + : null; + return manifestPath && fs.existsSync(manifestPath); +} + +export function shouldBuildOnSave(): boolean { + return config.buildOnSave() && swiftPackageExists(); +} + +export class Toolchain { + private swiftBinPath: string; + private basePath: string; + private buildArgs: string[]; + private buildProc?: ChildProc; + private runProc?: ChildProc; + private _diagnostics?: DiagnosticCollection; + + constructor(swiftPath: string, pkgBasePath: string, args: string[]) { + this.swiftBinPath = swiftPath; + this.basePath = pkgBasePath; + this.buildArgs = args; + } + + // Getters + get isRunning(): boolean { + return this.runProc != undefined; + } + + get diagnostics(): DiagnosticCollection { + if (!this._diagnostics) { + this._diagnostics = languages.createDiagnosticCollection("swift"); + } + return this._diagnostics; + } + + // Public API + /** + * @returns A Disposable that can be used to stop this instance of the Toolchain + */ + start(): Disposable { + return { + dispose: () => this.stop(), + }; + } + + /** + * Stops this instance of the Toolchain + */ + stop() { + if (this.buildProc) { + console.log("Stopping build proc"); + } + this.buildProc?.kill(); + if (this.runProc) { + console.log("Stopping run proc"); + } + this.runProc?.kill(); + } + + private spawnSwiftProc(args: string[], logs: LogStream, onExit: OnProcExit): ProcAndOutput { + // let oPipe = new Duplex({ highWaterMark: 1024, allowHalfOpen: false }); + // let ePipe = new Duplex({ highWaterMark: 1024, allowHalfOpen: false }); + const proc = cp.spawn(this.swiftBinPath, args, { + cwd: this.basePath, + // stdio: ["ignore", oPipe, ePipe], + }); + proc.stderr.on("data", data => { + logs.write(`${data}`); + }); + let stdout = ""; + proc.stdout.on("data", data => { + stdout += data; + logs.write(`${data}`); + }); + const promise = new Promise((resolve, reject) => { + logs.log(`pid: ${proc.pid} - ${this.swiftBinPath} ${args.join(" ")}`); + proc.on("error", err => { + logs.log(`[Error] ${err.message}`); + reject(err); + }); + proc.on("exit", (code, signal) => { + resolve(stdout); + onExit(code, signal); + }); + }); + return { proc, output: promise }; + } + + build(target: string = "") { + output.build.clear(); + output.build.log("-- Build Started --"); + const start = Date.now(); + const buildArgs = [...this.buildArgs]; + if (target) { + buildArgs.unshift(target); + } + buildArgs.unshift("build"); + statusBarItem.start(); + try { + const { proc, output: buildOutput } = this.spawnSwiftProc(buildArgs, output.build, code => { + const duration = Date.now() - start; + if (code != 0) { + statusBarItem.failed(); + output.build.log(`-- Build Failed (${(duration / 1000).toFixed(1)}s) --`, true); + } else { + statusBarItem.succeeded(); + output.build.log(`-- Build Succeeded (${(duration / 1000).toFixed(1)}s) --`); + } + this.buildProc = undefined; + }); + buildOutput.then(buildOutput => this.generateDiagnostics(buildOutput)); + this.buildProc = proc; + } catch (e) { + console.log(e); + } + } + + private generateDiagnostics(buildOutput: string = "") { + this._diagnostics.clear(); + const newDiagnostics = new Map(); + const lines = buildOutput.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const match = line.match(DiagnosticFirstLine); + if (!match) { + // console.log(`line did not match - '${line}'`); + continue; + // } else { + // console.log(`found diagnostic - + // ${line} + // ${lines[i + 1]} + // ${lines[i + 2]} + // -------`); + // console.log(match); + } + const [_, file, lineNumStr, startColStr, swiftSev, message] = match; + // vscode used 0 indexed lines and columns + const lineNum = parseInt(lineNumStr, 10) - 1; + const startCol = parseInt(startColStr, 10) - 1; + const endCol = lines[i + 2].trimEnd().length - 1; + const range = new Range(lineNum, startCol, lineNum, endCol); + const diagnostic = new Diagnostic(range, message, toVSCodeSeverity(swiftSev)); + diagnostic.source = "sourcekitd"; + if (!newDiagnostics.has(file)) { + newDiagnostics.set(file, []); + } + newDiagnostics.get(file).push(diagnostic); + } + for (const entry of newDiagnostics) { + const [file, diagnostics] = entry; + if (file.includes("checkouts")) { + continue; + } + // TODO: check for overlapping diagnostic ranges and collapse into `diagnostic.relatedInformation` + const uri = Uri.parse(file); + // TODO: check to see if sourcekitd already has diagnostics for this file + this._diagnostics.set(uri, diagnostics); + } + } + + runStart(target: string = "") { + setRunning(true); + output.run.clear(); + output.run.log(`running ${target ? target : "package"}…`); + const { proc } = this.spawnSwiftProc(["run", target], output.run, (code, signal) => { + // handle termination here + output.run.log(`Process exited. code=${code} signal=${signal}`); + setRunning(false); + this.runProc = undefined; + }); + this.runProc = proc; + } + + runStop() { + setRunning(false); + output.run.log(`stopping`); + this.runProc.kill(); + this.runProc = undefined; + } + + clean() { + statusBarItem.start("cleaning"); + this.spawnSwiftProc(["package clean"], output.build, (code, signal) => { + statusBarItem.succeeded("clean"); + output.build.log("done"); + }); + } +} + +function toVSCodeSeverity(sev: string) { + switch (sev) { + case "error": + return DiagnosticSeverity.Error; + case "warning": + return DiagnosticSeverity.Warning; + case "note": + return DiagnosticSeverity.Information; + default: + return DiagnosticSeverity.Hint; //FIXME + } +} diff --git a/src/vscode/config-helpers.ts b/src/vscode/config-helpers.ts new file mode 100644 index 0000000..0fd199e --- /dev/null +++ b/src/vscode/config-helpers.ts @@ -0,0 +1,129 @@ +import * as path from "path"; +import * as fs from "fs"; +import { workspace, ExtensionContext } from "vscode"; +import { ServerOptions, TransportKind, Executable } from "vscode-languageclient"; + +export enum LangaugeServerMode { + SourceKit = "sourcekit-lsp", + LanguageServer = "langserver", + SourceKite = "sourcekite", +} + +/** + * @returns which language server to use + */ +export function lsp(): LangaugeServerMode { + return workspace + .getConfiguration() + .get("sde.languageServerMode", LangaugeServerMode.SourceKit); +} + +/** + * @returns if the project should be built when a file is saved + */ +export function buildOnSave(): boolean { + return workspace.getConfiguration().get("sde.buildOnSave", true); +} + +/** + * @returns if build logging is enabled + */ +export function isBuildTracingOn(): boolean { + return workspace.getConfiguration().get("sde.enableTracing.client"); +} + +export function isLSPTracingOn(): boolean { + return workspace.getConfiguration().get("sde.enableTracing.LSPServer"); +} + +/** + * get server options for + * @param context the current extension context + */ +export function sourcekiteServerOptions(context: ExtensionContext): ServerOptions { + // The server is implemented in node + const serverModule = context.asAbsolutePath(path.join("out/src/sourcekites-server", "server.js")); + // The debug options for the server + const debugOptions = { + execArgv: ["--nolazy", "--inspect=6004"], + ...process.env, + }; + + // If the extension is launched in debug mode then the debug server options are used + // Otherwise the run options are used + const serverOptions: ServerOptions = { + run: { + module: serverModule, + transport: TransportKind.ipc, + options: debugOptions, + }, + debug: { + module: serverModule, + transport: TransportKind.ipc, + options: debugOptions, + }, + }; + return serverOptions; +} + +export function lspServerOptions(): ServerOptions { + // Load the path to the language server from settings + const executableCommand = workspace + .getConfiguration("swift") + .get("languageServerPath", "/usr/local/bin/LanguageServer"); + + const run: Executable = { + command: executableCommand, + options: process.env, + }; + const debug: Executable = run; + const serverOptions: ServerOptions = { + run: run, + debug: debug, + }; + return serverOptions; +} + +export function sourcekitLspServerOptions(): ServerOptions { + const toolchain = workspace.getConfiguration("sourcekit-lsp").get("toolchainPath"); + + const sourcekitPath = sourceKitLSPLocation(toolchain); + + // sourcekit-lsp takes -Xswiftc arguments like "swift build", but it doesn't need "build" argument + const sourceKitArgs = workspace + .getConfiguration() + .get("sde.swiftBuildingParams", []) + .filter(param => param !== "build"); + + const env: NodeJS.ProcessEnv = toolchain + ? { ...process.env, SOURCEKIT_TOOLCHAIN_PATH: toolchain } + : process.env; + + const run: Executable = { + command: sourcekitPath, + options: { env }, + args: sourceKitArgs, + }; + const serverOptions: ServerOptions = run; + return serverOptions; +} + +function sourceKitLSPLocation(toolchain: string | undefined): string { + const explicit = workspace + .getConfiguration("sourcekit-lsp") + .get("serverPath", null); + if (explicit) return explicit; + + const sourcekitLSPPath = path.resolve( + toolchain || "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain", + "usr/bin/sourcekit-lsp" + ); + const isPreinstalled = fs.existsSync(sourcekitLSPPath); + if (isPreinstalled) { + return sourcekitLSPPath; + } + + return workspace + .getConfiguration("swift") + .get("languageServerPath", "/usr/local/bin/sourcekit-lsp"); +} diff --git a/src/vscode/lsp-interop.ts b/src/vscode/lsp-interop.ts new file mode 100644 index 0000000..019fee7 --- /dev/null +++ b/src/vscode/lsp-interop.ts @@ -0,0 +1,90 @@ +import { workspace, ExtensionContext, Disposable } from "vscode"; +import { LanguageClient, LanguageClientOptions, ServerOptions } from "vscode-languageclient"; +import { absolutePath } from "../helpers/AbsolutePath"; +import * as config from "./config-helpers"; +import { LangaugeServerMode } from "./config-helpers"; + +function currentServerOptions(context: ExtensionContext): ServerOptions { + switch (config.lsp()) { + case LangaugeServerMode.LanguageServer: + return config.lspServerOptions(); + case LangaugeServerMode.SourceKit: + return config.sourcekitLspServerOptions(); + case LangaugeServerMode.SourceKite: + return config.sourcekiteServerOptions(context); + } +} + +function currentClientOptions(): Partial { + switch (config.lsp()) { + case LangaugeServerMode.SourceKit: + return { + documentSelector: ["swift", "cpp", "c", "objective-c", "objective-cpp"], + synchronize: undefined, + }; + case LangaugeServerMode.SourceKite: + return { + initializationOptions: { + isLSPServerTracingOn: config.isLSPTracingOn(), + skProtocolProcess: absolutePath( + workspace.getConfiguration().get("swift.path.sourcekite") + ), + skProtocolProcessAsShellCmd: workspace + .getConfiguration() + .get("swift.path.sourcekiteDockerMode"), + skCompilerOptions: workspace.getConfiguration().get("sde.sourcekit.compilerOptions"), + toolchainPath: + workspace.getConfiguration("sourcekit-lsp").get("toolchainPath") || null, + }, + }; + default: + return {}; + } +} + +let lspClient: LanguageClient | undefined; +let clientDisposable: Disposable | undefined; + +/** + * Starts the LSP client (which specifies how to start the LSP server), and registers + * a dispoasble in the extension context. + * @param context the SDE extension context + */ +function startLSPClient(context: ExtensionContext) { + let clientOptions: LanguageClientOptions = { + // Register the server for plain text documentss + documentSelector: [ + { language: "swift", scheme: "file" }, + { pattern: "*.swift", scheme: "file" }, + ], + synchronize: { + configurationSection: ["swift", "editor", "[swift]"], + // Notify the server about file changes to '.clientrc files contain in the workspace + fileEvents: [ + workspace.createFileSystemWatcher("**/*.swift"), + workspace.createFileSystemWatcher(".build/*.yaml"), + ], + }, + ...currentClientOptions(), + }; + // Create the language client and start the client. + const lspOpts = currentServerOptions(context); + lspClient = new LanguageClient("Swift", lspOpts, clientOptions); + clientDisposable = lspClient.start(); + context.subscriptions.push(clientDisposable); +} + +/** + * Stops the current LSP client and starts a new client. + * The client is stopped using the disposable returned from `client.start()` + * @param context the SDE extension context + */ +function restartLSPClient(context: ExtensionContext) { + clientDisposable.dispose(); + startLSPClient(context); +} + +export default { + startLSPClient, + restartLSPClient, +}; diff --git a/src/vscode/output-channels.ts b/src/vscode/output-channels.ts new file mode 100644 index 0000000..002a870 --- /dev/null +++ b/src/vscode/output-channels.ts @@ -0,0 +1,47 @@ +import { Disposable, ExtensionContext, window } from "vscode"; + +export interface LogStream { + write(msg: string, show?: boolean): void; + log(msg: string, show?: boolean): void; + clear(): void; +} + +let disposables: Disposable[] = []; +function makeChannel(name: string, showByDefault: boolean = true): LogStream { + const _channel = window.createOutputChannel(`Swift - ${name}`); + disposables.push(_channel); + + const retVal = { + _channel, + write(msg: string, show: boolean = showByDefault) { + this._channel.append(msg); + if (show) { + this._channel.show(true); + } + }, + log(msg: string, show: boolean = showByDefault) { + this._channel.appendLine(msg); + if (show) { + this._channel.show(true); + } + }, + clear() { + this._channel.clear(); + }, + }; + return retVal; +} + +function init(context: ExtensionContext) { + context.subscriptions.push(...disposables); +} + +export default { + init, + noop: { + log(msg: string, show?: boolean) {}, + clear() {}, + }, + build: makeChannel("Build", false), + run: makeChannel("Run"), +}; diff --git a/src/vscode/status-bar.ts b/src/vscode/status-bar.ts new file mode 100644 index 0000000..968cd99 --- /dev/null +++ b/src/vscode/status-bar.ts @@ -0,0 +1,49 @@ +import { window, StatusBarItem, StatusBarAlignment, ThemeColor } from "vscode"; + +const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +function buildAnimator() { + let i = 0; + return function() { + i = (i + 1) % frames.length; + return frames[i]; + }; +} + +let buildItem: StatusBarItem; +let defaultColor: string | ThemeColor; +let animationInterval: NodeJS.Timeout; + +const getItem = () => { + if (!buildItem) { + buildItem = window.createStatusBarItem(StatusBarAlignment.Left); + defaultColor = buildItem.color; + } + buildItem.color = defaultColor; + buildItem.show(); + return buildItem; +}; +const stopAnimation = () => clearInterval(animationInterval); + +export const statusBarItem = { + start(action: string = "building") { + stopAnimation(); + const item = getItem(); + const nextFrame = buildAnimator(); + animationInterval = setInterval(() => { + item.text = `${nextFrame()} ${action}`; + }, 100); + }, + succeeded(action: string = "build") { + stopAnimation(); + const item = getItem(); + item.text = `$(check) ${action} succeeded`; + item.color = defaultColor; + setTimeout(() => item.hide(), 10000); + }, + failed(action: string = "build") { + stopAnimation(); + const item = getItem(); + item.text = `$(issue-opened) ${action} failed`; + item.color = "red"; + }, +}; diff --git a/tsconfig.json b/tsconfig.json index ef93279..5cc8f1f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,9 +4,10 @@ "target": "es6", "outDir": "out", "lib": ["es6"], - "sourceMap": true, - "rootDir": "." + "rootDir": "src", + "sourceMap": true + // "strict": true, + // "noUnusedLocals": true }, - "exclude": ["node_modules", "server", "out"], "compileOnSave": true } diff --git a/yarn.lock b/yarn.lock index 1566836..9de0bc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -383,10 +383,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.2.tgz#ace1880c03594cc3e80206d96847157d8e7fa349" integrity sha512-bnoqK579sAYrQbp73wwglccjJ4sfRdKU7WNEZ5FW4K2U6Kc0/eZ5kvXG0JKsEKFB50zrFmfFt52/cvBbZa7eXg== -"@types/node@^12.7.4": - version "12.12.30" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.30.tgz#3501e6f09b954de9c404671cefdbcc5d9d7c45f6" - integrity sha512-sz9MF/zk6qVr3pAnM0BSQvYIBK44tS75QC5N+VbWSE4DjCV/pJ+UzCW/F+vVnl7TkOPcuwQureKNtSSwjBTaMg== +"@types/node@^12.19.7": + version "12.19.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.11.tgz#9220ab4b20d91169eb78f456dbfcbabee89dfb50" + integrity sha512-bwVfNTFZOrGXyiQ6t4B9sZerMSShWNsGRw8tC5DY1qImUNczS9SjT4G6PnzjCnxsu5Ubj6xjL2lgwddkxtQl5w== "@types/stack-utils@^1.0.1": version "1.0.1" @@ -3237,6 +3237,11 @@ ts-jest@^24.0.2: semver "^5.5" yargs-parser "10.x" +tsc@^1.20150623.0: + version "1.20150623.0" + resolved "https://registry.yarnpkg.com/tsc/-/tsc-1.20150623.0.tgz#4ebc3c774e169148cbc768a7342533f082c7a6e5" + integrity sha1-Trw8d04WkUjLx2inNCUz8ILHpuU= + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -3256,10 +3261,10 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -typescript@^3.6.2: - version "3.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" - integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== +typescript@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" + integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== union-value@^1.0.0: version "1.0.1"