from Intelligent code completion for matlab by Leif Persson
Code completion for the Matlab editor.

Matlab_Code_Completion_Macro(hDocument,eventData)
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                        

Contact us at files@mathworks.com