/** * jumptodef Module for the Cloud9 * * @copyright 2013, Ajax.org B.V. */ define(function(require, exports, module) { main.consumes = [ "Plugin", "tabManager", "ace", "language", "menus", "commands", "c9", "tabManager", "ui", "settings", "preferences", "proc", "dialog.error" ]; main.provides = ["language.jumptodef"]; return main; function main(options, imports, register) { var Plugin = imports.Plugin; var language = imports.language; var commands = imports.commands; var settings = imports.settings; var aceHandle = imports.ace; var prefs = imports.preferences; var tabs = imports.tabManager; var ui = imports.ui; var proc = imports.proc; var showError = imports["dialog.error"].show; var c9 = imports.c9; var util = require("plugins/c9.ide.language/complete_util"); var HoverLink = require("./hover_link").HoverLink; var MouseHandler = require("ace/mouse/mouse_handler").MouseHandler; var useragent = require("ace/lib/useragent"); var menus = imports.menus; var CRASHED_JOB_TIMEOUT = 30000; var worker; var loaded; var lastJump; /***** Initialization *****/ var plugin = new Plugin("Ajax.org", main.consumes); function load() { if (loaded) return false; loaded = true; language.once("initWorker", function(e) { worker = e.worker; commands.addCommand({ name: "jumptodef", bindKey: { mac: "F3", win: "F3" }, hint: "jump to the definition of the variable or function that is under the cursor", exec: function() { jumptodef(); } }, plugin); // right click context item in ace var mnuJumpToDef = new ui.item({ id: "mnuEditorJumpToDef", caption: "Jump to Definition", command: "jumptodef" }); var mnuJumpToDef2 = new ui.item({ caption: "Jump to Definition", command: "jumptodef", id: "mnuEditorJumpToDef2" }); aceHandle.getElement("menu", function(menu) { menus.addItemToMenu(menu, mnuJumpToDef2, 750, plugin); menu.on("prop.visible", function(e) { // only fire when visibility is set to true if (e.value) { // because of delays we'll disable by default mnuJumpToDef2.disable(); checkIsJumpToDefAvailable(); } }); }); menus.addItemByPath("Goto/Jump to Definition", mnuJumpToDef, 1450, plugin); // when the context menu pops up we'll ask the worker whether we've // jumptodef available here // listen to the worker's response worker.on("definition", function(e) { onDefinitions(e); }); // when the analyzer tells us if the jumptodef result is available // we'll disable/enable the jump to definition item in the ctx menu worker.on("isJumpToDefinitionAvailableResult", function(ev) { if (ev.data.value) { mnuJumpToDef2.enable(); } else { mnuJumpToDef2.disable(); } var ace = tabs.focussedTab && tabs.focussedTab.editor && tabs.focussedTab.editor.ace; if (ace && ace.hoverLink && ace.hoverLink.isOpen) { var pos = ev.data.pos; if (!ev.data.value) { ace.hoverLink.range.contains(pos.row, pos.column); ace.hoverLink.removeMarker(); } } }); }); language.on("attachToEditor", function addBinding(ace) { // ace.$mouseHandler.$enableJumpToDef = true; var hoverLink = new HoverLink(ace); hoverLink.on("addMarker", function (e) { worker.emit("isJumpToDefinitionAvailable", { data: e.range.start }); }); hoverLink.on("open", function(e, hoverLink) { if (hoverLink.isOpen) { var cursor = hoverLink.editor.getCursorPosition(); if (!hoverLink.range.contains(cursor.row, cursor.column)) hoverLink.editor.selection.setRange(hoverLink.range); jumptodef(); } }); }); settings.on("read", function () { settings.setDefaults("user/language", [ ["overrideMultiselectShortcuts", "true"] ]); updateSettings(); }); prefs.add({ "Language": { "Input": { "Use Cmd-Click for Jump to Definition": { type: "checkbox", path: "user/language/@overrideMultiselectShortcuts", position: 6000 } } } }, plugin); settings.on("user/language", updateSettings); } function updateSettings() { var key = useragent.isMac ? "cmd-" : "ctrl-"; if (settings.getBool("user/language/@overrideMultiselectShortcuts")) { MouseHandler.prototype.$enableJumpToDef = true; HoverLink.prototype.$keyModifier = key; } else { MouseHandler.prototype.$enableJumpToDef = false; HoverLink.prototype.$keyModifier = key + "shift-"; } } function addUnknownColumn(ace, pos, name) { if (pos.sc) return pos; var document = ace.document || ace.getSession().getDocument(); if (!document) return pos; var line = document.getLine(pos.sl); if (!line) return pos; if (!name) { pos.sc = line.match(/^(\s*)/).length; return pos; } var index = line && line.indexOf(name); if (index < 0) return pos; pos.sc = index; pos.el = pos.el || pos.sl; if (pos.el === pos.sl) pos.ec = index + name.length; return pos; } /** * Fire an event to the worker that asks whether the jumptodef is available for the * current position. * Fires an 'isJumpToDefinitionAvailableResult' event on the same channel when ready */ function checkIsJumpToDefAvailable() { var ace = tabs.focussedTab.editor.ace; if (!ace) return; worker.emit("isJumpToDefinitionAvailable", { data: ace.getSelection().getCursor() }); } function jumptodef() { if (!tabs.focussedTab || !tabs.focussedTab.editor || !tabs.focussedTab.editor.ace) return; var tab = tabs.focussedTab; var ace = tab.editor.ace; var sel = ace.getSelection(); var pos = sel.getCursor(); activateSpinner(tabs.focussedTab); onJumpStart(ace); if (lastJump && lastJump.ace === ace && lastJump.row === pos.row && lastJump.column === pos.column) { clearSpinners(tab); jumpToPos(lastJump.sourcePath, lastJump.sourcePos); return; } lastJump = null; worker.emit("jumpToDefinition", { data: pos }); } function onDefinitions(e) { var tab = tabs.findTab(e.data.path); if (!tab) return; clearSpinners(tab); var results = e.data.results; var editor = tab.editor; if (!results.length) return onJumpFailure(e, editor.ace); // We have no UI for multi jumptodef; we just take the last for now var lastResult; for (var i = results.length - 1; i >= 0; i--) { lastResult = results[i]; if (!lastResult.isGeneric) break; } var path = lastResult && lastResult.path || tab.path; jumpToPos(path, lastResult, e.data.path, e.data.pos); } function jumpToPos(path, pos, sourcePath, sourcePos, callback) { pos.row = pos.row || 0; pos.column = pos.column || 0; if (path.substr(0, 2) === "//") { if (path.indexOf(c9.workspaceDir + "/") === 1) path = path.substr(c9.workspaceDir.length + 1); else if (path.indexOf(c9.homeDir + "/") === 1) path = "~" + path.substr(c9.homeDir.length + 1); else // HACK: read file outside of vfs roots return proc.execFile("cat", { args: [path.substr(1)]}, function(err, result) { if (err) return showError("Could not open refrenced file: " + path); openTab(result); }); } if (path[0] !== "/" && path[0] !== "~") { path = "/" + path; } openTab(); function openTab(nonVFSValue) { tabs.open( { path: path, active: true, value: nonVFSValue }, function(err, tab) { if (err) return; if (nonVFSValue) { makeReadonly(tab); if (tab.document) tab.document.meta.closeOnError = true; } var state = tab.document && tab.document.getState(); if (state && state.ace) { pos = addUnknownColumn(tab.editor.ace, pos); lastJump = sourcePos && { ace: tab.editor.ace, row: pos.row, column: pos.column, path: path, sourcePos: sourcePos, sourcePath: sourcePath }; state.ace.jump = { row: pos.row, column: pos.column }; } delete state.value; tab.document.setState(state); tabs.focusTab(tab); callback && callback(); } ); } } function makeReadonly(tab) { tab.editor.ace.setReadOnly(true); tab.editor.ace.session.on("changeEditor", function(e) { if (e.oldEditor) { e.oldEditor.setReadOnly(false); } if (e.editor) { e.editor.setReadOnly(true); } }); } function onJumpFailure(event, ace) { // Add a short delay as additional feedback setTimeout(function() { var cursor = ace.getSelection().getCursor(); var oldPos = event.data.pos; if (oldPos.row !== cursor.row || oldPos.column !== cursor.column) return; var line = ace.getSession().getLine(oldPos.row); if (!line) return; var preceding = util.retrievePrecedingIdentifier(line, cursor.column); var column = cursor.column - preceding.length; if (column === oldPos.column) column = line.match(/^(\s*)/).length; var newPos = { row: cursor.row, column: column }; ace.getSelection().setSelectionRange({ start: newPos, end: newPos }); }, 300); } function onJumpStart(ace) { var cursor = ace.getSelection().getCursor(); var line = ace.getSession().getDocument().getLine(cursor.row); if (!line) return; var preceding = util.retrievePrecedingIdentifier(line, cursor.column); var column = cursor.column - preceding.length; var following = util.retrieveFollowingIdentifier(line, column); var startPos = { row: cursor.row, column: column }; var endPos = { row: cursor.row, column: column + following.length }; ace.getSelection().setSelectionRange({ start: startPos, end: endPos }); } function activateSpinner(tab) { tab.classList.add("loading"); clearTimeout(tab.$jumpToDefReset); tab.$jumpToDefReset = setTimeout(function() { clearSpinners(tab); }, CRASHED_JOB_TIMEOUT); } function clearSpinners(tab) { clearTimeout(tab.$jumpToDefReset); tab.classList.remove("loading"); } plugin.on("load", function() { load(); }); plugin.on("enable", function() { }); plugin.on("disable", function() { }); plugin.on("unload", function() { loaded = false; }); register(null, { "language.jumptodef": plugin.freezePublicAPI({ /** @ignore */ addUnknownColumn: addUnknownColumn, /** @ignore */ jumpToPos: jumpToPos }) }); } });