diff --git a/js/connection.js b/js/connection.js index a8f813d..1d150ca 100644 --- a/js/connection.js +++ b/js/connection.js @@ -528,6 +528,25 @@ weechat.factory('connection', }); }; + var requestCompletion = function(bufferId, position, data) { + // Prevent requesting completion if bufferId is invalid + if (!bufferId) { + return; + } + + return ngWebsockets.send( + weeChat.Protocol.formatCompletion({ + buffer: "0x" + bufferId, + position: position, + data: data + }) + ).then(function(message) { + return new Promise(function (resolve) { + resolve( handlers.handleCompletion(message) ); + }); + }); + }; + return { connect: connect, @@ -538,7 +557,8 @@ weechat.factory('connection', sendHotlistClearAll: sendHotlistClearAll, fetchMoreLines: fetchMoreLines, requestNicklist: requestNicklist, - attemptReconnect: attemptReconnect + attemptReconnect: attemptReconnect, + requestCompletion: requestCompletion }; }]); })(); diff --git a/js/handlers.js b/js/handlers.js index 9891e8f..16d6288 100644 --- a/js/handlers.js +++ b/js/handlers.js @@ -491,6 +491,12 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific }); }; + var handleCompletion = function(message) { + var completionInfo = message.objects[0].content[0]; + + return completionInfo; + }; + var eventHandlers = { _buffer_closing: handleBufferClosing, _buffer_line_added: handleBufferLineAdded, @@ -529,7 +535,8 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific handleLineInfo: handleLineInfo, handleHotlistInfo: handleHotlistInfo, handleNicklist: handleNicklist, - handleBufferInfo: handleBufferInfo + handleBufferInfo: handleBufferInfo, + handleCompletion: handleCompletion }; }]); diff --git a/js/inputbar.js b/js/inputbar.js index 3380530..380429b 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -31,6 +31,9 @@ weechat.directive('inputBar', function() { // Emojify input. E.g. Turn :smile: into the unicode equivalent, but // don't do replacements in the middle of a word (e.g. std::io::foo) $scope.inputChanged = function() { + // Cancel any command completion that was still ongoing + commandCompletionInputChanged = true; + var emojiRegex = /^(?:[\uD800-\uDBFF][\uDC00-\uDFFF])+$/, // *only* emoji changed = false, // whether a segment was modified inputNode = $scope.getInputNode(), @@ -76,6 +79,13 @@ weechat.directive('inputBar', function() { }; $scope.completeNick = function() { + if ((models.version[0] == 2 && models.version[1] >= 9 || models.version[0] > 2) && + $scope.command.startsWith('/') ) { + // We are completing a command, another function will do + // this on WeeChat 2.9 and later + return; + } + // input DOM node var inputNode = $scope.getInputNode(); @@ -108,6 +118,124 @@ weechat.directive('inputBar', function() { }, 0); }; + var previousInput; + var commandCompletionList; + var commandCompletionAddSpace; + var commandCompletionBaseWord; + var commandCompletionPosition; + var commandCompletionPositionInList; + var commandCompletionInputChanged; + $scope.completeCommand = function(direction) { + if (models.version[0] < 2 || (models.version[0] == 2 && models.version[1] < 9)) { + // Command completion is only supported on WeeChat 2.9+ + return; + } + + if ( !$scope.command.startsWith('/') ) { + // We are not completing a command, maybe a nick? + return; + } + + // Cancel if input changes + commandCompletionInputChanged = false; + + // input DOM node + var inputNode = $scope.getInputNode(); + + // get current caret position + var caretPos = inputNode.selectionStart; + + // get current active buffer + var activeBuffer = models.getActiveBuffer(); + + // Empty input makes $scope.command undefined -- use empty string instead + var input = $scope.command || ''; + + // This function is for later cycling the list after we got it + var cycleCompletionList = function (direction) { + // Don't do anything, the input has changed before we were able to complete the command + if ( commandCompletionInputChanged ) { + return; + } + + // Check if the list has elements and we have not cycled to the end yet + if ( !commandCompletionList || !commandCompletionList[0] ) { + return; + } + + // If we are cycling in the other direction, go back two placed in the list + if ( direction === 'backward' ) { + commandCompletionPositionInList -= 2; + + if ( commandCompletionPositionInList < 0 ) { + // We have reached the beginning of list and are going backward, so go to the end; + commandCompletionPositionInList = commandCompletionList.length - 1; + } + } + + // Check we have not reached the end of the cycle list + if ( commandCompletionList.length <= commandCompletionPositionInList ) { + // We have reached the end of the list, start at the beginning + commandCompletionPositionInList = 0; + } + + // Cycle the list + // First remove the word that's to be completed + var commandBeforeReplace = $scope.command.substring(0, commandCompletionPosition - commandCompletionBaseWord.length); + var commandAfterReplace = $scope.command.substring(commandCompletionPosition, $scope.command.length); + var replacedWord = commandCompletionList[commandCompletionPositionInList]; + var suffix = commandCompletionAddSpace ? ' ' : ''; + + // Fill in the new command + $scope.command = commandBeforeReplace + replacedWord + suffix + commandAfterReplace; + + // Set the cursor position + var newCursorPos = commandBeforeReplace.length + replacedWord.length + suffix.length; + setTimeout(function() { + inputNode.focus(); + inputNode.setSelectionRange(newCursorPos, newCursorPos); + }, 0); + + // If there is only one item in the list, we are done, no next cycle + if ( commandCompletionList.length === 1) { + previousInput = ''; + return; + } + // Setup for the next cycle + commandCompletionPositionInList++; + commandCompletionBaseWord = replacedWord + suffix; + previousInput = $scope.command + activeBuffer.id; + commandCompletionPosition = newCursorPos; + } + + // Check if we have requested this completion info before + if (input + activeBuffer.id !== previousInput) { + // Remeber we requested this input for next time + previousInput = input + activeBuffer.id; + + // Ask weechat for the completion list + connection.requestCompletion(activeBuffer.id, caretPos, input).then( function(completionObject) { + // Save the list of completion object, we will only request is once + // and cycle through it as long as the input doesn't change + commandCompletionList = completionObject.list; + commandCompletionAddSpace = completionObject.add_space + commandCompletionBaseWord = completionObject.base_word; + commandCompletionPosition = caretPos; + commandCompletionPositionInList = 0; + }).then( function () { + //after we get the list we can continue with our first cycle + cycleCompletionList(direction); + }); + + + } else { + // Input hasn't changed so we should already have our completion list + cycleCompletionList(direction); + } + }; + + + $rootScope.insertAtCaret = function(toInsert) { // caret position in the input bar var inputNode = $scope.getInputNode(), @@ -367,10 +495,18 @@ weechat.directive('inputBar', function() { } // Tab -> nick completion - if (code === 9 && !$event.altKey && !$event.ctrlKey) { + if (code === 9 && !$event.altKey && !$event.ctrlKey && !$event.shiftKey) { $event.preventDefault(); $scope.iterCandidate = tmpIterCandidate; $scope.completeNick(); + $scope.completeCommand('forward'); + return true; + } + + // Shitft-Tab -> nick completion backward (only commands) + if (code === 9 && !$event.altKey && !$event.ctrlKey && $event.shiftKey) { + $event.preventDefault(); + $scope.completeCommand('backward'); return true; } @@ -606,6 +742,7 @@ weechat.directive('inputBar', function() { $scope.handleCompleteNickButton = function($event) { $event.preventDefault(); $scope.completeNick(); + $scope.completeCommand('forward'); setTimeout(function() { $scope.getInputNode().focus(); diff --git a/js/weechat.js b/js/weechat.js index f46e3ef..2b99c25 100644 --- a/js/weechat.js +++ b/js/weechat.js @@ -777,6 +777,33 @@ return WeeChatProtocol._formatCmd(params.id, 'input', parts); }; + /** + * Formats a completion command. + * https://weechat.org/files/doc/stable/weechat_relay_protocol.en.html#command_completion + * @param params Parameters: + * id: command ID (optional) + * buffer: target buffer (mandatory) + * position: position for completion in string (optional) + * data: input data (optional) + * @return Formatted input command string + */ + WeeChatProtocol.formatCompletion = function(params) { + var defaultParams = { + id: null, + position: -1 + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + parts.push(params.buffer); + parts.push(params.position); + if (params.data) { + parts.push(params.data); + } + + return WeeChatProtocol._formatCmd(params.id, 'completion', parts); + }; + /** * Formats a sync or a desync command. *