From 1368142d0e71ce4b49fb97d094dd7ea5f8e9acce Mon Sep 17 00:00:00 2001 From: Jeremy Mahieu Date: Tue, 28 Apr 2020 22:17:27 +0200 Subject: [PATCH 1/6] Command completion for weechat 2.9 --- js/connection.js | 22 +++++++++- js/handlers.js | 9 +++- js/inputbar.js | 107 ++++++++++++++++++++++++++++++++++++++++++++++- js/weechat.js | 27 ++++++++++++ 4 files changed, 162 insertions(+), 3 deletions(-) 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..86dd518 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -76,6 +76,11 @@ weechat.directive('inputBar', function() { }; $scope.completeNick = function() { + if ( $scope.command.startsWith('/') ) { + // We are completing a command, an other function will do this + return; + } + // input DOM node var inputNode = $scope.getInputNode(); @@ -108,6 +113,98 @@ weechat.directive('inputBar', function() { }, 0); }; + var previousInput; + var commandCompletionList; + var commandCompletionBaseWord; + var commandCompletionPosition; + var commandCompletionPositionInList; + $scope.completeCommand = function(direction) { + if ( !$scope.command.startsWith('/') ) { + // We are not completing a command, maybe a nick? + return; + } + + // 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) { + // 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 + commandCompletionBaseWord.length); + var replacedWord = commandCompletionList[commandCompletionPositionInList]; + $scope.command = commandBeforeReplace + replacedWord + commandAfterReplace; + + // Set the cursor position + var newCursorPos = commandBeforeReplace.length + replacedWord.length; + setTimeout(function() { + inputNode.focus(); + inputNode.setSelectionRange(newCursorPos, newCursorPos); + }, 0); + + // Setup for the next cycle + commandCompletionPositionInList++; + commandCompletionBaseWord = replacedWord; + previousInput = $scope.command + activeBuffer.id; + commandCompletionPosition = $scope.command.length; + } + + // 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; + 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 +464,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; + } + + // Tab -> nick completion + if (code === 9 && !$event.altKey && !$event.ctrlKey && $event.shiftKey) { + $event.preventDefault(); + $scope.completeCommand('backward'); return true; } 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. * From b47a6576acb8e297a16788ef485c0bb0758aa059 Mon Sep 17 00:00:00 2001 From: Jeremy Mahieu Date: Tue, 28 Apr 2020 22:23:57 +0200 Subject: [PATCH 2/6] Also add command completion with the mobile button --- js/inputbar.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/inputbar.js b/js/inputbar.js index 86dd518..56fdc37 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -711,6 +711,7 @@ weechat.directive('inputBar', function() { $scope.handleCompleteNickButton = function($event) { $event.preventDefault(); $scope.completeNick(); + $scope.completeCommand('forward'); setTimeout(function() { $scope.getInputNode().focus(); From e9633bf721b766e6cdbde8bfffa89094b6ab8383 Mon Sep 17 00:00:00 2001 From: Jeremy Mahieu Date: Tue, 28 Apr 2020 22:30:50 +0200 Subject: [PATCH 3/6] Edit comment for shift-tab --- js/inputbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/inputbar.js b/js/inputbar.js index 56fdc37..b72d541 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -472,7 +472,7 @@ weechat.directive('inputBar', function() { return true; } - // Tab -> nick completion + // Shitft-Tab -> nick completion backward (only commands) if (code === 9 && !$event.altKey && !$event.ctrlKey && $event.shiftKey) { $event.preventDefault(); $scope.completeCommand('backward'); From f8608f47380a2297df2f75121f8dc4b2436d46e8 Mon Sep 17 00:00:00 2001 From: Jeremy Mahieu Date: Wed, 29 Apr 2020 19:40:17 +0200 Subject: [PATCH 4/6] Fix partial completions, add space, cancel on input --- js/inputbar.js | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/js/inputbar.js b/js/inputbar.js index b72d541..b817ad9 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(), @@ -115,15 +118,20 @@ weechat.directive('inputBar', function() { var previousInput; var commandCompletionList; + var commandCompletionAddSpace; var commandCompletionBaseWord; var commandCompletionPosition; var commandCompletionPositionInList; + var commandCompletionInputChanged; $scope.completeCommand = function(direction) { 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(); @@ -138,6 +146,11 @@ weechat.directive('inputBar', function() { // 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; @@ -162,12 +175,15 @@ weechat.directive('inputBar', function() { // 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 + commandCompletionBaseWord.length); + var commandAfterReplace = $scope.command.substring(commandCompletionPosition, $scope.command.length); var replacedWord = commandCompletionList[commandCompletionPositionInList]; - $scope.command = commandBeforeReplace + replacedWord + commandAfterReplace; + var suffix = commandCompletionAddSpace ? ' ' : ''; + + // Fill in the new command + $scope.command = commandBeforeReplace + replacedWord + suffix + commandAfterReplace; // Set the cursor position - var newCursorPos = commandBeforeReplace.length + replacedWord.length; + var newCursorPos = commandBeforeReplace.length + replacedWord.length + suffix.length; setTimeout(function() { inputNode.focus(); inputNode.setSelectionRange(newCursorPos, newCursorPos); @@ -175,9 +191,9 @@ weechat.directive('inputBar', function() { // Setup for the next cycle commandCompletionPositionInList++; - commandCompletionBaseWord = replacedWord; + commandCompletionBaseWord = replacedWord + suffix; previousInput = $scope.command + activeBuffer.id; - commandCompletionPosition = $scope.command.length; + commandCompletionPosition = newCursorPos; } // Check if we have requested this completion info before @@ -190,6 +206,7 @@ weechat.directive('inputBar', function() { // 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; @@ -197,6 +214,8 @@ weechat.directive('inputBar', 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); From de41b7c0fac0eafda298f6b2e329f5661edb1774 Mon Sep 17 00:00:00 2001 From: Jeremy Mahieu Date: Wed, 29 Apr 2020 20:10:36 +0200 Subject: [PATCH 5/6] Stop completion if only one possibility --- js/inputbar.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/js/inputbar.js b/js/inputbar.js index b817ad9..26d2c89 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -189,6 +189,11 @@ weechat.directive('inputBar', function() { 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; From 4bacb850cf97169e12f78c14d1b4c26b60b3efb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Thu, 30 Apr 2020 11:20:09 +0200 Subject: [PATCH 6/6] Check weechat version --- js/inputbar.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/js/inputbar.js b/js/inputbar.js index 26d2c89..380429b 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -79,8 +79,10 @@ weechat.directive('inputBar', function() { }; $scope.completeNick = function() { - if ( $scope.command.startsWith('/') ) { - // We are completing a command, an other function will do this + 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; } @@ -124,6 +126,11 @@ weechat.directive('inputBar', function() { 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;