function Matlab_Code_Completion_Macro(hDocument,eventData)
% Matlab_Code_Completion_Macro
% Intelligent code completion for Matlab
% By Leif Persson,
% Swedish Defence Research Agency FOI,
% Division of CBRN Protection and Security,
% Ume, Sweden
%
% Inspired by and using EditorMacro by Yair Altman
% To use this macro, install EditorMacro and insert the following
% in your startup.m file
%
% macros = EditorMacro('ctrl-space',@Matlab_Code_Completion_Macro, 'run');
% macros = EditorMacro('ctrl-alt-space',@Matlab_Code_Completion_Macro, 'run');
% macros = EditorMacro('shift-ctrl-space',@Matlab_Code_Completion_Macro, 'run');
%
% Use ctrl-space to step through the matched completion strings
% Use ctrl-shift-space to step in the opposite order
% Use ctrl-alt-space to choose the current completion string
%
% Also, ctrl-alt-space can be used to conveniently step through the
% parts of a function definition, or a if, elseif, for or while statement
% This behavior is triggered by ctrl-alt-space at the end of the
% corresponding keyword (function, if, elseif, for, while) or at the end of
% a variable list or condition
%
% TIPS: use ctrl-z to remove completion suggestion
% TIPS: use ctrl-i to get correct indentation
% TODO: Evaluate performance on large documents.
% TODO: Sorting of matched tokens weighted by frequency
% TODO: Popup menu for completions
%
doDebugPrint=false;
debugLevel=1;
debugPrint('Doing Matlab_Code_Completion_Macro...', 1);
persistent currMatchedTokenNum numMatchedTokens matchedTokens sortedMatchedTokens;
matlabKeywords = iskeyword;
matlabKeywords = [ matlabKeywords; 'properties'; 'methods' ];
CR=char(13); % Carriage return
LF=char(10); % Line feed
TAB=char(9); % Horizontal tab
leading_var_char='[a-zA-Z\._@]';
var_char='(\w|[\._@])';
var_name=[ '(' leading_var_char '(' var_char ')*)' ];
var_seq=['(' ... % either
'\s*' ... % blank
'|' ... % or
'\s*' var_name '\s*'...% variable name ...
'(,\s*' var_name '\s*)*'... % optionally followed by comma-separated varnames
')'];
fun_name=var_name;
%% Get document tokens
docLength=getLength(hDocument);
docText=getTextStartEnd(hDocument, 0, docLength);
% Extract document lines
m=regexp(docText, '([^\n]*)[\n]?', 'tokens');
docLines=[m{:}]; % cell array of matched pattern ([^\n]*) tokens
debugPrint('Document lines:', 1);
debugPrint(docLines, 1);
% Add % at the beginning of lines within block comments
isInBlockComment=false;
numDocLines=length(docLines);
for docLineNum=1:numDocLines,
if(regexp(docLines{docLineNum}, '^\s*%{\s*$')),
isInBlockComment=true;
elseif(regexp(docLines{docLineNum}, '^\s*%}\s*$')),
isInBlockComment=false;
elseif(isInBlockComment),
docLines{docLineNum}=regexprep(docLines{docLineNum}, '(.*)', '%$1');
end
end
debugPrint('Added % at the beginning of lines within block comments:', 1);
debugPrint(docLines, 1);
% Extract lines with code
m=regexp(docLines, '^\s*([^%\s]+)%?.*$', 'match');
codeDocLines=[m{:}];
debugPrint('Code document lines:', 1);
debugPrint(codeDocLines, 1);
% Strip off comments at the end of code lines
m=regexp(codeDocLines, '^\s*([^%]+)%.*|([^%]+)$', 'tokens');
commentStrippedDocLines=cellfun(@(n) n{1}, m);
debugPrint('Comment-stripped document lines:', 1);
debugPrint(commentStrippedDocLines, 1);
% Strip off strings
% TODO: Handle multline strings?
m=regexp(commentStrippedDocLines, '(\''([^\'']|\''\'')*\'')', 'split');
strippedDocLines=[m{:}];
strippedDocLines=regexprep(strippedDocLines, '(.*)', '$1\n'); % add newline
% Construct document text with comments and strings stripped off
strippedDocText=[ strippedDocLines{:} ];
debugPrint('Got stripped document text:', 1);
debugPrint(strippedDocText, 1);
% Extract document tokens
m=regexp(strippedDocText, var_name, 'match'); % Cell arrays of strings, but vector
[docTokens, all2unique, unique2all]=unique([ matlabKeywords(:)' m]); % syntax also works
debugPrint('Found document tokens:', 1);
debugPrint(docTokens, 1);
maxDocTokenLength=max(cellfun('length', docTokens));
debugPrint(['Max document token length is ' num2str(maxDocTokenLength)], 1);
indexCount=@(index) length(find(unique2all==index));
docTokensIndices=1:length(docTokens);
docTokensCounts=arrayfun(indexCount, docTokensIndices);
debugPrint('Document tokens counts:', 1);
debugPrint(docTokensCounts, 2);
%% Get current line up to caret position
prevEOLPos=getPrevEOLPosition(hDocument);
currentPos=getCaretPosition(hDocument);
str=getTextStartEnd(hDocument, prevEOLPos, currentPos);
%% Process current line
% Check if completion list is up
pat=[ var_name '(<<--{' var_name '})$' ];
debugPrint([ 'Matching pattern <' pat '>' ], 1);
m=regexp(str,pat,'match');
debugPrint('Finished match...', 1);
if(eventData.isAltDown && isempty(m)), % no completion list
%% Match composite statements (if, elseif, else, function etc...
%% if statement
debugPrint('Matching if...', 1);
pat='^(i|^.*\si)f\s*$';
if regexp(str, pat),
insertStringBeforeCaret(hDocument, ' ( ');
return;
end
pat='^(i|^.*\si)f\s*\(.*$';
if regexp(str, pat),
insertStringBeforeCaret(hDocument, ' )');
numLeadingTabs=getNumLeadingTabs(hDocument);
insertStringBeforeCaret(hDocument, LF); %CR
for i=1:(numLeadingTabs+1)
insertStringBeforeCaret(hDocument, TAB); % TAB
end
return;
end
%% elseif statement
debugPrint('Matching elseif...', 1);
pat='^(e|^.*\se)lseif\s*$';
if regexp(str, pat),
insertStringBeforeCaret(hDocument, ' ( ');
return;
end;
debugPrint('Matching elseif ( ...', 1);
pat='^((e|^.*\se)lseif)\s*\(.*$';
if (regexp(str,pat)),
insertStringBeforeCaret(hDocument, ' )');
numLeadingTabs=getNumLeadingTabs(hDocument);
insertStringBeforeCaret(hDocument, LF); %CR
for i=1:(numLeadingTabs+1)
insertStringBeforeCaret(hDocument, TAB); % TAB
end
return;
end
%% for statement
debugPrint('Matching for...', 1);
pat='^(f|^.*\sf)or\s*$';
if regexp(str, pat),
insertStringBeforeCaret(hDocument, ' ( ');
return;
end
pat='^(f|^.*\sf)or\s*\(.*$';
if regexp(str, pat),
insertStringBeforeCaret(hDocument, ' )');
numLeadingTabs=getNumLeadingTabs(hDocument);
insertStringBeforeCaret(hDocument, LF); %CR
for i=1:(numLeadingTabs+1)
insertStringBeforeCaret(hDocument, TAB); % TAB
end
return;
end
%% while statement
debugPrint('Matching while ...', 1);
pat='^(w|^.*\sw)hile\s*$';
if regexp(str, pat),
insertStringBeforeCaret(hDocument, ' ( ');
return;
end
pat='^(w|^.*\sw)hile\s*\(.*$';
if regexp(str, pat),
insertStringBeforeCaret(hDocument, ' )');
numLeadingTabs=getNumLeadingTabs(hDocument);
insertStringBeforeCaret(hDocument, LF); %CR
for i=1:(numLeadingTabs+1)
insertStringBeforeCaret(hDocument, TAB); % TAB
end
return;
end
%% function definition
debugPrint('Matching function...', 1);
pat='^(f|.*\sf)unction\s*$';
debugPrint(['Match string ''' str ''' for ''function'''], 1);
if regexp(str, pat),
debugPrint('Match found!', 1);
insertStringBeforeCaret(hDocument, ' [ ');
return;
end;
pat=[ '^(f|.*\sf)unction\s+\[' var_seq '(\s*)$' ];
debugPrint(['Match string ''' str ''' for ''function [ '''], 1);
if regexp(str, pat),
debugPrint('Match found!', 1);
insertStringBeforeCaret(hDocument, ' ] = ');
return;
end;
pat=[ '^(f|.*\sf)unction\s+\[' var_seq '\]\s*=\s*' fun_name '(\s*)$' ];
debugPrint(['Match string ''' str ''' for ''function [ ... ] = ...'''], 1);
if regexp(str, pat),
debugPrint('Match found!', 1);
insertStringBeforeCaret(hDocument, '( ');
return;
end;
pat=[ '^(f|.*\sf)unction\s+\[' var_seq '\]\s*=\s*' fun_name '\s*\(' var_seq '(\s*)$' ];
debugPrint(['Match string ''' str ''' for ''function [ ... ] = ... ( ...'''], 1);
if regexp(str, pat),
debugPrint('Match found!', 1);
insertStringBeforeCaret(hDocument, ' )');
numLeadingTabs=getNumLeadingTabs(hDocument);
insertStringBeforeCaret(hDocument, LF); %CR
for i=1:(numLeadingTabs+1)
insertStringBeforeCaret(hDocument, TAB); % TAB
end
return;
end
% None of the above; insert end statement
debugPrint('No statement match found, insert end statement...', 1);
insertStringBeforeCaret(hDocument, LF);
insertStringBeforeCaret(hDocument, 'end');
end
%% Tokens completion a la TextMate
debugPrint('Doing token completion...', 1);
pat=[ var_name '$' ];
debugPrint([ 'Matching pattern <' pat '>' ], 1);
m=regexp(str,pat,'match'); % m{i} contains match number i
if ((~isempty(m)) && ~eventData.isAltDown), % match occured
comp_str=m{1}; % One match because EOL pattern anchor $
debugPrint([ 'Found completion string <' comp_str '>' ], 1);
numMatchedChars=length(comp_str);
% Generate pattern string to collect matching document tokens
comp_pat=regexprep(comp_str, '(\S)', '\\S\*$1');
comp_pat=[ '(' comp_pat '\S*)' ];
% Replace '.' by '\.' in pattern
comp_pat=regexprep(comp_pat, '(\.)', '\\$1');
debugPrint([ 'Built completion pattern <' comp_pat '>' ], 1);
m=regexpi(docTokens, comp_pat, 'match');
% docTokens{i} constains string m{i}{j} contains match j
% from docTokens{i}. However j>1 is excluded because EOL anchor
% pattern $
matchedTokens=[m{:}]; % Collect all matches in cell array matchedTokens
numMatchedTokens=length(matchedTokens);
debugPrint(['Found ' num2str(numMatchedTokens) ' matched document tokens:'], 1);
debugPrint(matchedTokens, 1);
findDocToken=@(token) find(ismember(docTokens, token)==1, 1, 'first');
matchedTokensIndices=cellfun(findDocToken, matchedTokens);
debugPrint('Matched tokens indices:', 1);
debugPrint(matchedTokensIndices, 2);
matchedTokensCounts=docTokensCounts(matchedTokensIndices);
debugPrint('Matched tokens counts:', 1);
debugPrint(matchedTokensCounts, 1);
%% Compute weghts of matched document tokens for sorting
weights=zeros(numMatchedTokens,1);
base=maxDocTokenLength;
factors=base.^(numMatchedChars:-1:0);
% Generate pattern string for collecting matched pattern tokens
comp_pat=regexprep(comp_str, '(\S)', '\(\[\^$1\]\*\)\($1\)');
% Example: comp_str='abc' generates the completion pattern
% ([^a]*)(a)([^b]*)(b)([^c](c)(\S*)
%
% This pattern is too greedy:
% comp_pat=regexprep(comp_str, '(\S)', '\(\\S\*\)\($1\)');
% Example: comp_str='abc' generates the completion pattern
% (\S*)(a)(\S*)(b)(\S*)(c)(\S*)
comp_pat=[ comp_pat '(\S*)' ];
debugPrint([ 'Built completion pattern <' comp_pat '>' ], 1);
% Compute weights for all matched tokens
for j=1:numMatchedTokens,
debugPrint(['Processing <' matchedTokens{j} '>'], 2);
m=regexpi(matchedTokens{j}, comp_pat, 'tokenExtents');
tmp=m{:}; % A two-column array containing the extent of the
% pattern tokens.
% Example: m=regexp('abcdefghi', '(\S*)(e)(\S*)(f)(\S*)', 'tokenExtents')
% gives: m{1}= [ 1 4; 5 5; 4 5; 6 5; 7 9 ]
gap_extents=tmp(1:2:end,2)-tmp(1:2:end,1)+1;
debugPrint('Computed gaps extents:', 2);
debugPrint(gap_extents, 2);
% The example above gives
% gap_extents= [4 0 3]
num_nonzero_gaps=length(find(gap_extents~=0));
position_weight=dot(factors, gap_extents);
weights(j)=position_weight ...
+num_nonzero_gaps*base^(numMatchedChars+1);
% Put highest weight on number of nonzero gaps
end
debugPrint('Computed weights vector', 1);
debugPrint(weights, 2);
[sortedWeights, sortedIndices]=sort(weights);
debugPrint('Computed sorted weight vector and indices', 1);
debugPrint([sortedWeights sortedIndices], 2);
sortedMatchedTokens=cell(size(matchedTokens));
sortedMatchedTokensCounts=zeros(size(matchedTokens));
debugPrint('Sorting cell array...', 2);
for j=1:length(matchedTokens),
sortedMatchedTokens{j}=matchedTokens{sortedIndices(j)};
sortedMatchedTokensCounts(j)=matchedTokensCounts(sortedIndices(j));
end;
debugPrint('Sorted matched tokens:', 1);
debugPrint(sortedMatchedTokens, 1);
debugPrint('Sorted matched tokens counts:', 1);
debugPrint(sortedMatchedTokensCounts, 1);
% Remove first match if only one occurence
if (sortedMatchedTokensCounts(1)==1),
debugPrint('Removing first match...', 2);
sortedMatchedTokens=sortedMatchedTokens(2:end); % NOTE: () not {} (why?)
sortedMatchedTokensCounts=sortedMatchedTokensCounts(2:end);
end
%% Display matched tokens
currMatchedTokenNum=1;
currMatchedToken=char(sortedMatchedTokens(currMatchedTokenNum));
numMatchedTokens=length(sortedMatchedTokens);
debugPrint('Current matched token is:', 1);
debugPrint(currMatchedToken, 1);
% TODO
if (numMatchedTokens>1),
insertStr=[ '<<--{' currMatchedToken '}' ];
else
debugPrint([ 'Deleting string <' comp_str '>' ], 1);
deleteStringBeforeCaret(hDocument, length(comp_str));
insertStr=currMatchedToken;
end
debugPrint('String to insert before caret:', 1);
debugPrint(insertStr, 1);
insertStringBeforeCaret(hDocument, insertStr);
return;
end
%% Suggested completion
pat=[ var_name '(<<--{' var_name '})$' ];
debugPrint([ 'Matching pattern <' pat '>' ], 1);
m=regexp(str,pat,'tokens');
debugPrint('Finished match...', 1);
if (~isempty(m{1})),
debugPrint([ 'Deleting string <' m{1}{2} '>' ], 1);
deleteStringBeforeCaret(hDocument, length(m{1}{2}));
if (eventData.isAltDown)
debugPrint([ 'Deleting string <' m{1}{1} '>' ], 1);
deleteStringBeforeCaret(hDocument, length(m{1}{1}));
currMatchedToken=char(sortedMatchedTokens(currMatchedTokenNum));
debugPrint([ 'Inserting string <' currMatchedToken '>'], 1 )
insertStringBeforeCaret(hDocument, currMatchedToken);
else
if (eventData.isShiftDown)
currMatchedTokenNum=mod(currMatchedTokenNum-2, numMatchedTokens)+1;
else
currMatchedTokenNum=mod(currMatchedTokenNum, numMatchedTokens)+1;
end
currMatchedToken=char(sortedMatchedTokens(currMatchedTokenNum));
debugPrint('Current matched token is:', 1);
debugPrint(currMatchedToken, 1);
insertStr=[ '<<--{' currMatchedToken '}' ];
debugPrint('String to insert before caret:', 1);
debugPrint(insertStr, 1);
insertStringBeforeCaret(hDocument, insertStr);
end
return;
end
%% Auxiliary local function (mainly java wrappers)
function pos=getCaretPosition(hDocument)
try
debugPrint('getCaretPosition: Trying to get position...', 3);
pos=javaMethod('getCaretPosition', hDocument.java);
debugPrint(['getCaretPosition: Succcessfully got caret position ' num2str(pos)], 3);
catch
debugPrint('Warning: getCaretPosition: Could not get position, using 0 ...', 3)
pos=0;
end
end
function len=getLength(hDocument)
try
debugPrint('getLength: Trying to get document length ...', 3);
len=javaMethod('getLength', hDocument.java);
debugPrint(['getLength: Succcessfully got document length ' num2str(len)], 3);
catch
debugPrint('Warning: getLength: Could not get document length, using caret position ...', 3)
pos=getCaretPosition(hDocument);
end
end
function numTabs=getNumLeadingTabs(hDocument)
debugPrint('getNumLeadingTabs: trying to get number of leading tabs of current line...', 3);
currentPos=getCaretPosition(hDocument);
prevEOLPos=getPrevEOLPosition(hDocument);
debugPrint(['getNumLeadingTabs: found previous EOL position ' num2str(prevEOLPos)], 3);
debugPrint(['getNumLeadingTabs: getting text from previous EOL to current position ' num2str(currentPos)], 3);
textFromBOL=getTextStartEnd(hDocument, prevEOLPos, currentPos);
debugPrint(['getNumLeadingTabs: found leading text <' textFromBOL '>'], 3);
tabPositions=regexp(textFromBOL, '\t');
numTabs=0;
if (length(tabPositions)>0),
for i=1:length(tabPositions)
if (tabPositions(i)>i) break; end
numTabs=i;
end
end
debugPrint(['getNumLeadingTabs: found ' num2str(numTabs) ' leading tabs on current line ...'], 3);
end
function str=getTextStartEnd(hDocument, startPos, endPos)
debugPrint(['getTextStartEnd: Trying to get text from position ' num2str(startPos) ' to ' num2str(endPos)], 3);
try
jstr=javaMethod('getTextStartEnd', hDocument.java, startPos, endPos);
str=char(jstr);
debugPrint(['getTextStartEnd: Succcessfully got text <' str '>'], 3);
catch
debugPrint('Warning: getTextStartEnd: Could not get text, returning empty string ...', 3);
str='';
end
end
function pos=getNextEOLPosition(hDocument)
debugPrint('getNextEOLPosition: trying to get next EOL ...', 3);
try
currentPos=getCaretPosition(hDocument);
docLength=getLength(hDocument);
textToEOF=char(getTextStartEnd(hDocument, currentPos, docLength));
pos=currentPos+find(textToEOF<=13,1,'first')-1; % next CR/LF
if isempty(pos)
pos=docLength; % EOL=EOF
debugPrint(['getNextEOLPosition: Next EOL position is EOF ' num2str(pos)], 3); ...
else ...
debugPrint(['getNextEOLPosition: Next EOL position is ' num2str(pos)], 3);
end
catch
debugPrint('getNextEOLPosition: Warning: Failed to get next EOL, using caret position ...', 3);
pos=getCaretPosition(hDocument);
end
end
function pos=getPrevEOLPosition(hDocument)
debugPrint('getPrevEOLPosition: trying to get previous EOL ...', 3);
try
currentPos=getCaretPosition(hDocument);
debugPrint('getPrevEOLPosition: get text from BOF...', 3);
tmp=getTextStartEnd(hDocument, 0, currentPos);
textFromBOF=char(tmp);
debugPrint('getPrevEOLPosition: find last EOL', 3);
pos=find(textFromBOF<=13,1,'last'); % previous CR/LF
if isempty(pos)
pos=0; % Beginning of file
debugPrint(['getPrevEOLPosition: Previoius EOL position is BOF ' num2str(pos)], 3);
else
debugPrint(['getPrevEOLPosition: Previous EOL position is ' num2str(pos)], 3);
end
catch
debugPrint('getPrevEOLPosition: Warning: Failed to get previous EOL, using beginning of document ...', 3);
pos=0;
end
end
function str=getTextBeforeCaret(hDocument, len)
debugPrint('getTextBeforeCaret: Entering ...', 3);
currentPos=getCaretPosition(hDocument);
try
debugPrint('getTextBeforeCaret: Trying to get string ...', 3);
len=min(currentPos, len);
%jstr=javaMethod('getTextStartEnd', hDocument.java, currentPos-len,currentPos);
str=getTextStartEnd(hDocument, currentPos-len, currentPos);
debugPrint(['getTextBeforeCaret: got string <' str '>!'], 3);
catch
debugPrint('Warning: getTextBeforeCaret: Could not get string, using empty string ...', 3)
str='';
end
end
function deleteStringBeforeCaret(hDocument, len)
debugPrint('deleteStringBeforeCaret: Entering ...', 3);
currentPos=getCaretPosition(hDocument);
try
len=min(currentPos,len);
debugPrint(['deleteStringBeforeCaret: Trying to delete string of length ' num2str(len), 3]);
javaMethod('delete', hDocument.java, currentPos-len, currentPos);
debugPrint('deleteStringBeforeCaret: Successfully deleted string, now setting caret position...', 3)
javaMethod('setCaretPosition', hDocument.java, currentPos-len);
debugPrint('deleteStringBeforeCaret: Successfull, exiting...', 3)
catch
debugPrint('Warning: deleteStringBeforeCaret: Failed to delete, exiting ...', 3)
end
end
function insertStringBeforeCaret(hDocument, str)
currentPos=getCaretPosition(hDocument);
debugPrint(['insertStringBeforeCaret: Trying to insert string <' str '>'], 3);
try
jstr=javaObject('java.lang.String', str);
javaMethod('insert', hDocument.java, currentPos, jstr);
debugPrint('insertStringBeforeCaret: Successfully inserted string!', 3);
catch
debugPrint('Warning: insertStringBeforeCaret: Could not insert before caret', 3);
return;
end
debugPrint('insertStringBeforeCaret: set caret to end of string', 3);
javaMethod('setCaretPosition', hDocument.java, currentPos+length(str));
end
function debugPrint(debugStr, varargin)
if (nargin==1),
doPrint = doDebugPrint;
else
doPrint = (doDebugPrint && (varargin{1}<=debugLevel));
end
if (doPrint),
disp(debugStr);
end
end
function res=flatCell(n)
res=n{:};
end
end % Matlab_Code_Completion_Macro