AMApplicationBuild 428 AMApplicationVersion 2.7 AMDocumentVersion 2 actions action AMAccepts Container List Optional Types com.apple.applescript.object AMActionVersion 1.0 AMApplication Automator AMParameterProperties source AMProvides Container List Types com.apple.applescript.object ActionBundlePath /System/Library/Automator/Run JavaScript.action ActionName Run JavaScript ActionParameters source /* * Copyright (C) 2017 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ ObjC.import("Cocoa"); var g_isSVN; var g_isGit; var g_isGitSVN; var g_lastSVNInfo; var App = Application.currentApplication(); App.includeStandardAdditions = true; function run(input, parameters) { var xcodeDocument = xcodeActiveDocument(); if (!xcodeDocument) return; var xcodeDocumentPath = xcodeDocument.path(); determineVCSFromPath(xcodeDocumentPath); if (!pathIsInWebKitCheckout(xcodeDocumentPath)) return; var lineNumber = xcodeSelectedLineInDocument(xcodeDocument); var path = pathRelativeToRepositoryRootForPath(xcodeDocumentPath); var revisionInfo = revisionInfoForPath(xcodeDocumentPath); var annotateBlame = $.NSEvent.modifierFlags & $.NSAlternateKeyMask; App.setTheClipboardTo(permalinkForPath(path, lineNumber, revisionInfo, annotateBlame)); } function pathIsInWebKitCheckout(path) { var repositoryURL = revisionInfoForPath(path).repositoryURL; return !!repositoryURL.match(/^\w+:\/\/\w+\.webkit.org/); } function permalinkForPath(path, lineNumber, revisionInfo, annotateBlame) { var revision = revisionInfo.revision ? "?rev=" + revisionInfo.revision : ""; var lineNumber = lineNumber ? "#L" + lineNumber : ""; var branch = revisionInfo.branch || "trunk"; var withBlame = annotateBlame ? "&annotate=blame" : ""; return `https://trac.webkit.org/browser/${branch}/${path}${revision}${withBlame}${lineNumber}`; } // MARK: Xcode function xcodeActiveDocument() { var xcode = Application("Xcode"); var windows = xcode.windows(); var numberOfWindows = windows.length; if (!numberOfWindows) return null; // The title of an Xcode Workspace window is the title of the document in the editor pane. // Ignore windows without a name (e.g. "Edit all occurrences of a symbol" pop-up menu). var documentName; for (var i = 0; !documentName && i < numberOfWindows; ++i) documentName = windows[i].name(); if (!documentName) return null; // The title of a modified document that has not been saved will have a suffix. Remove // the suffix. const editedSuffix = " — Edited"; if (documentName.endsWith(editedSuffix)) documentName = documentName.substr(0, documentName.lastIndexOf(editedSuffix)); return xcode.documents.byName(documentName); } function xcodeSelectedLineInDocument(xcodeDocument) { if (!xcodeDocument) return -1; var range = xcodeDocument.selectedCharacterRange(); if (!range) return -1; var beginPosition = range[0] - 1; if (!beginPosition) return 0; // FIXME: It would be more efficient to count the CRLF, CR, or LF characters // in the substring from [0, beginPosition]. var lines = xcodeDocument.text().split(/\r?\n|\r/); var numberOfLines = lines.length; var characterCount = 0; var i = 0; do { characterCount += lines[i].length + 1; if (characterCount > beginPosition) break; } while (++i < numberOfLines); return i + 1; } // MARK: VCS utilities function determineVCSFromPath(path) { if (!isDirectory(path)) path = dirname(path); g_isSVN = false; g_isGit = false; g_isGitSVN = false; if (isSVNDirectory(path)) { g_isSVN = true; return; } if (isGitSVNDirectory(path)) { g_isGit = true; g_isGitSVN = true; return; } if (isGitDirectory(path)) { g_isGit = true; return; } } function pathRelativeToRepositoryRootForPath(path) { var directoryInCheckout = isDirectory(path) ? path : dirname(path); if (g_isSVN) return svnPathRelativeToRepositoryRootForPath(path, directoryInCheckout); if (g_isGit) return gitPathRelativeToRepositoryRootForPath(path, directoryInCheckout); return ""; } function gitPathRelativeToRepositoryRootForPath(path, directoryInCheckout) { return App.doShellScript(`git -C '${directoryInCheckout}' ls-tree --full-name --name-only HEAD '${path}'`); } function svnPathRelativeToRepositoryRootForPath(path, directoryInCheckout) { return svnInfoForPath(path, directoryInCheckout).path; } function revisionInfoForPath(path) { var directoryInCheckout = isDirectory(path) ? path : dirname(path); if (g_isSVN || g_isGitSVN) return svnRevisionInfoForPath(path, directoryInCheckout); if (g_isGit) return gitRevisionInfoForPath(path, directoryInCheckout); return ""; } function svnRevisionInfoForPath(path, directoryInCheckout) { var svnInfo = svnInfoForPath(path, directoryInCheckout); return { "branch": svnInfo.branch, "revision": svnInfo.revision, "repositoryURL": svnInfo.repositoryRoot }; } function gitRevisionInfoForPath(path, directoryInCheckout) { var repositoryURL = App.doShellScript(`git -C '${directoryInCheckout}' remote get-url origin`); var revision = App.doShellScript(`git -C '${directoryInCheckout}' log -1 --format='%H' '${path}'`); var branch = App.doShellScript(`git -C '${directoryInCheckout}' symbolic-ref -q HEAD`); branch = branch.replace(/^refs\/heads\//, "") || "master"; return { branch, revision, repositoryURL }; } function svnInfoForPath(path, directoryInCheckout) { if (g_lastSVNInfo && g_lastSVNInfo.path === path) { // FIXME: We should also ensure that the checkout directory for the cached SVN info is // the same as the specified checkout directory. return g_lastSVNInfo; } var svnInfoCommand = "svn info"; if (g_isGitSVN) svnInfoCommand = "git " + svnInfoCommand; var output = App.doShellScript(`cd '${directoryInCheckout}' && ${svnInfoCommand} '${path}'`, {"alteringLineEndings": false}); if (!output) return { }; var temp = { }; var lines = output.split("\n"); for (var line of lines) { var [key, value] = line.split(": ", 2); if (key && value) temp[key] = value; } var svnInfo = { "pathAsURL": temp["URL"], "repositoryRoot": temp["Repository Root"], "revision": temp["Revision"], }; var branch = svnInfo.pathAsURL.replace(svnInfo.repositoryRoot + "/", ""); branch = branch.substr(0, branch.indexOf("/")); svnInfo.branch = branch; // Although tempting to use temp["Path"] we cannot because it is relative to directoryInCheckout. // And directoryInCheckout may not be the top-level checkout directory. We need to compute the // relative path with respect to the top-level checkout directory. svnInfo.path = svnInfo.pathAsURL.replace(`${svnInfo.repositoryRoot}/${branch}/`, ""); g_lastSVNInfo = svnInfo; return svnInfo; } function isSVNDirectory(directory) { try { App.doShellScript(`cd '${directory}' && svn info > /dev/null 2>&1`); return true; } catch (e) { return false; } } function isGitDirectory(directory) { try { App.doShellScript(`git -C '${directory}' rev-parse > /dev/null 2>&1`); return true; } catch (e) { return false; } } function isGitSVNDirectory(directory) { var output = ""; try { output = App.doShellScript(`git -C '${directory}' config --get svn-remote.svn.fetch 2>&1`); } catch (e) { } return output !== ""; } // MARK: Utilities function isDirectory(path) { try { return App.infoFor(path).folder; } catch (e) { return false; } } function dirname(path) { return path.substr(0, path.lastIndexOf("/")); } BundleIdentifier com.apple.Automator.RunJavaScript CFBundleVersion 1.0 CanShowSelectedItemsWhenRun CanShowWhenRun Category AMCategoryUtilities Class Name RunJavaScriptAction InputUUID 0C0655EF-7893-4A61-ADD0-BA803AF3C2CD Keywords Run JavaScript OutputUUID 5BAD8148-07E0-4FA2-AAA1-990A7BE926FC UUID 24BFD6CC-7A96-42C2-8469-5D83FA921DB2 UnlocalizedApplications Automator arguments 0 default value function run(input, parameters) { // Your script goes here return input; } name source required 0 type 0 uuid 0 isViewVisible location 480.500000:316.000000 nibPath /System/Library/Automator/Run JavaScript.action/Contents/Resources/Base.lproj/main.nib isViewVisible connectors workflowMetaData serviceApplicationBundleID com.apple.dt.Xcode serviceApplicationPath /Applications/Xcode.app serviceInputTypeIdentifier com.apple.Automator.nothing serviceOutputTypeIdentifier com.apple.Automator.nothing serviceProcessesInput 0 workflowTypeIdentifier com.apple.Automator.servicesMenu