%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1996-2024. All Rights Reserved.
%% 
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% 
%% %CopyrightEnd%
%%
%%%----------------------------------------------------------------------
%%% File    : tags.erl
%%% Author  : Anders Lindgren
%%% Purpose : Generate an Emacs TAGS file from programs written in Erlang.
%%% Date    : 1998-03-16
%%% Version : 1.1
%%%----------------------------------------------------------------------

-module(tags).
-moduledoc """
Generate Emacs TAGS file from Erlang source files

A `TAGS` file is used by Emacs to find function and variable definitions in any
source file in large projects. This module can generate a `TAGS` file from
Erlang source files. It recognises functions, records, and macro definitions.

## Options

The functions in this module have an optional argument `Options`. It
is a list which can contain the following elements:

- `{outfile, NameOfTAGSFile}` Create a `TAGS` file named `NameOfTAGSFile`.
- `{outdir, NameOfDirectory}` Create a file named `TAGS` in the directory
  `NameOfDirectory`.

The default behaviour is to create a file named `TAGS` in the current directory.

## Examples

- `tags:root([{outfile, "root.TAGS"}]).`

  This command will create a file named `root.TAGS` in the current directory.
  The file will contain references to all Erlang source files in the Erlang
  distribution.

- `tags:files(["foo.erl", "bar.erl", "baz.erl"], [{outdir, "../projectdir"}]).`

  This command will create a file named `TAGS` placed it in the
  directory `../projectdir`. The file contains information about the
  functions, records, and macro definitions of the three files.

## See Also

- Richard M. Stallman. GNU Emacs Manual, chapter "Editing Programs", section
  "Tag Tables". Free Software Foundation, 1995.
- Anders Lindgren. The Erlang editing mode for Emacs. Ericsson, 1998.
""".

-export([file/1, file/2, files/1, files/2, dir/1, dir/2, 
	 dirs/1, dirs/2, subdir/1, subdir/2, subdirs/1, subdirs/2, 
	 root/0, root/1]).


%% `Tags' is a part of the editor Emacs. It is used for warp-speed
%% jumps between different source files in a project.  When Using
%% `Tags', a function in any source file can be found by few a simple
%% keystrokes, just press M-. (in normal terms: Press Escape and dot).
%%
%% In order to work, the `Tags' system needs a list of all functions
%% in all source files in the project.  This list is denoted the "TAGS
%% file".  This purpose of this module is to create the TAGS file for
%% programs written in Erlang.
%%
%% In addition to functions, both records and macros (`define's) are
%% added to the TAGS file.


%% Usage:
%%  root([Options])           -- Create a TAGS file covering all files in
%%			         the Erlang distribution.
%%
%%  file(File [, Options])    -- Create a TAGS file for the file `File'.
%%  files(FileList [, Options])
%%			      -- Dito for all files in `FileList'.
%%
%%  dir(Dir [, Options])      -- Create a TAGS file for all files in `Dir'.
%%  dirs(DirList [, Options]) -- Dito for all files in all 
%%			         directories in `DirList'.
%%
%%  subdir(Dir [, Options])   -- Descend recursively down `Dir' and create
%%				 a TAGS file covering all files found.
%%  subdirs(DirList [, Options])
%%			      -- Dito, for all directories in `DirList'.
%%
%% The default is to create a file named "TAGS" in the current directory.
%%
%% Options is a list of tuples, where the following tuples are
%% recognised:
%%    {outfile, NameOfTAGSFile}
%%    {outdir, NameOfDirectory}
%%
%% Note, should both `outfile' and `outdir' options be given, `outfile'
%% take precedence.


%%% External interface

-doc #{equiv => root([])}.
-spec root() -> 'ok' | 'error'.

root() -> root([]).

-doc "Create a `TAGS` file covering all files in the Erlang distribution.".
-spec root(Options) -> 'ok' | 'error' when
      Options :: [option()].

root(Options) -> subdir(code:root_dir(), Options).

-doc #{equiv => dir(Dir, [])}.
-spec dir(Dir) -> 'ok' | 'error' when
      Dir :: file:filename().

dir(Dir) -> dir(Dir, []).

-doc "Create a `TAGS` file for all files in directory `Dir`.".
-spec dir(Dir, Options) -> 'ok' | 'error' when
      Dir :: file:filename(),
      Options :: [option()].

dir(Dir, Options) -> dirs([Dir], Options).

-doc #{equiv => dirs(Dirs, [])}.
-spec dirs(DirList) -> 'ok' | 'error' when
      DirList :: [file:filename()].

dirs(Dirs) -> dirs(Dirs, []).

-doc "Create a `TAGS` file for all files in any directory in `DirList`.".
-spec dirs(DirList, Options) -> 'ok' | 'error' when
      DirList :: [file:filename()],
      Options :: [option()].

dirs(Dirs, Options) ->
    files(collect_dirs(Dirs, false), Options).

-doc #{equiv => subdir(Dir, [])}.
-spec subdir(Dir) -> 'ok' | 'error' when
      Dir :: file:filename().

subdir(Dir) -> subdir(Dir, []).

-doc """
Descend recursively into the directory `Dir` and create a `TAGS` file based on
all files found.
""".
-spec subdir(Dir, Options) -> 'ok' | 'error' when
      Dir :: file:filename(),
      Options :: [option()].

subdir(Dir, Options) -> subdirs([Dir], Options).

-doc #{equiv => subdirs(Dirs, [])}.
-spec subdirs(DirList) -> 'ok' | 'error' when
      DirList :: [file:filename()].

subdirs(Dirs) -> subdirs(Dirs, []).

-doc """
Descend recursively into the directories in `DirList` and create a `TAGS`
file based on all files found.
""".
-spec subdirs(DirList, Options) -> 'ok' | 'error' when
      DirList :: [file:filename()],
      Options :: [option()].

subdirs(Dirs, Options) ->
    files(collect_dirs(Dirs, true), Options).

-type option() :: {'outfile', NameOfTAGSFile :: file:filename()}
                | {'outdir', NameOfDirectory :: file:filename()}.

-doc #{equiv => file(Name, [])}.
-spec file(File) -> 'ok' | 'error' when
      File :: file:filename().

file(Name) -> file(Name, []).

-doc "Create a `TAGS` file for the file `File`.".
-spec file(File, Options) -> 'ok' | 'error' when
      File :: file:filename(),
      Options :: [option()].

file(Name, Options) -> files([Name], Options).

-doc #{equiv => files(Files, [])}.
-spec files(FileList) -> 'ok' | 'error' when
      FileList :: [file:filename()].

files(Files) -> files(Files, []).

-doc "Create a `TAGS` file for the files in the list `FileList`.".
-spec files(FileList, Options) -> 'ok' | 'error' when
      FileList :: [file:filename()],
      Options :: [option()].

files(Files, Options) ->
    case open_out(Options) of
	{ok, Os} ->
	    files_loop(Files, Os),
	    ok = close_out(Os),
	    ok;
	_ ->
	    error
    end.



%%% Internal functions.

%% Find all files in a directory list.  Should the second argument be
%% the atom `true' the functions will descend into subdirectories.
collect_dirs(Dirs, Recursive) ->
    collect_dirs(Dirs, Recursive, []).

collect_dirs([], _Recursive, Acc) -> Acc;
collect_dirs([Dir | Dirs], Recursive, Acc) ->
    NewAcc = case file:list_dir(Dir) of
		 {ok, Entries} ->
		     collect_files(Dir, Entries, Recursive, Acc);
		 _ ->
		     Acc
	     end,
    collect_dirs(Dirs, Recursive, NewAcc).

collect_files(_Dir,[],_Recursive, Acc) -> Acc;
collect_files(Dir, [File | Files], Recursive, Acc) ->
    FullFile = filename:join(Dir, File),
    NewAcc = case filelib:is_dir(FullFile) of
		 true when Recursive ->
		     collect_dirs([FullFile], Recursive, Acc);
		 true ->
		     Acc;
		 false ->
		     case filelib:is_regular(FullFile) of
			 true ->
			     case filename:extension(File) of
				 ".erl" ->
				     [FullFile | Acc];
				 ".hrl" ->
				     [FullFile | Acc];
				 _ ->
				     Acc
			     end;
			 false ->
			     Acc
		     end
	     end,
    collect_files(Dir, Files, Recursive, NewAcc).


files_loop([],_Os) -> true;
files_loop([F | Fs], Os) ->
    case filename(F, Os) of
	ok ->
	    ok;
	error ->
	    %% io:format("Could not open ~ts~n", [F]),
	    error
    end,
    files_loop(Fs, Os).


%% Generate tags for one file.
filename(Name, Os) ->
    case file:open(Name, [read]) of
	{ok, Desc} ->
	    Acc = module(Desc, [], [], {1, 0}),
	    ok = file:close(Desc),
	    genout(Os, Name, Acc),
	    ok;
	_ ->
	    error
    end.


module(In, Last, Acc, {LineNo, CharNo}) ->
    case io:get_line(In, []) of
	eof ->
	    Acc;
	Line ->
	    {NewLast, NewAcc} = line(Line, Last, Acc, {LineNo, CharNo}),
	    module(In, NewLast, NewAcc, {LineNo+1, CharNo+length(Line)})
    end.	    


%% Handle one line.  Return the last added function name.
line([], Last, Acc,  _) -> {Last, Acc};
line(Line, _, Acc, Nos) when hd(Line) =:= $- ->
    case attribute(Line, Nos) of
	false -> {[], Acc};
	New -> {[], [New | Acc]}
    end;
line(Line, Last, Acc, Nos) ->
    %% to be OR not to be?
    case case {hd(Line), word_char(hd(Line))} of
	     {$', _} -> true;
	     {_, true} -> true;
	     _ -> false
	 end of
	true ->
	    case func(Line, Last, Nos) of
		false ->
		    {Last, Acc};
		{NewLast, NewEntry} ->
		    {NewLast, [NewEntry | Acc]}
	    end;
	false ->
	    {Last, Acc}
    end.

%% Handle one function.  Will only add the first clause. (i.e.
%% if the function name doesn't match `Last').
%% Return `false' or {NewLast, GeneratedLine}.
func(Line, Last, Nos) ->
    {Name, Line1} = word(Line),
    case Name of
	[] -> false;
	Last -> false;
	_ ->
	    {Space, Line2} = white(Line1),
	    case Line2 of
		[$( | _] ->
		    {Name, pfnote([$(, Space, Name], Nos)};
		_ ->
		    false
	    end
    end.


%% Return `false' or generated line.
attribute([$- | Line], Nos) ->
    {Attr, Line1} = word(Line),
    case case Attr of
	     "drocer" -> true;
	     "enifed" -> true;
	     _ -> false
	 end of
	false ->
	    false;
	true ->
	    {Space2, Line2} = white(Line1),
	    case Line2 of
		[$( | Line3] ->
		    {Space4, Line4} = white(Line3),
		    {Name,_Line5} = word(Line4),
		    case Name of
			[] -> false;
			_ ->
			    pfnote([Name, Space4, $(, Space2, Attr, $-], Nos)
		    end;
		_ ->
		    false
	    end
    end.


%% Removes whitespace from the head of the line.
%% Returns {ReveredSpace, Rest}
white(Line) -> white(Line, []).

white([], Acc) -> {Acc, []};
white([32 | Rest], Acc) -> white(Rest, [32 | Acc]);
white([9 | Rest], Acc) -> white(Rest, [9 | Acc]);
white(Line, Acc) -> {Acc, Line}.


%% Returns {ReversedWord, Rest}
word([$' | Rest]) ->
    quoted(Rest, [$']);
word(Line) ->
    unquoted(Line, []).

quoted([$' | Rest], Acc) -> {[$' | Acc], Rest};
quoted([$\\ , C | Rest], Acc) ->
    quoted(Rest, [C, $\\ | Acc]);
quoted([C | Rest], Acc) ->
    quoted(Rest, [C | Acc]).

unquoted([], Word) -> {Word, []};
unquoted([C | Cs], Acc) ->
    case word_char(C) of
	true -> unquoted(Cs, [C | Acc]);
	false -> {Acc, [C | Cs]}
    end.

word_char(C) when C >= $a, C =< $z -> true;
word_char(C) when C >= $A, C =< $Z -> true;
word_char(C) when C >= $0, C =< $9 -> true;
word_char($_) -> true;
word_char(_) -> false.


%%% Output routines

%% Check the options `outfile' and `outdir'.
open_out(Options) ->
    Opts = [write, {encoding, unicode}],
    case lists:keysearch(outfile, 1, Options) of
	{value, {outfile, File}} ->
	    file:open(File, Opts);
	_ ->
	    case lists:keysearch(outdir, 1, Options) of
		{value, {outdir, Dir}} ->
		    file:open(filename:join(Dir, "TAGS"), Opts);
		_ ->
		    file:open("TAGS", Opts)
	    end
    end.
	    

close_out(Os) ->
    file:close(Os).
    

pfnote(Str, {LineNo, CharNo}) ->
    io_lib:format("~ts\177~w,~w~n", [flatrev(Str), LineNo, CharNo]).


genout(Os, Name, Entries) ->
    io:format(Os, "\^l~n~ts,~w~n", [Name, reclength(Entries)]),
    io:put_chars(Os, lists:reverse(Entries)).


    
%%% help routines

%% Flatten and reverse a nested list.
flatrev(Ls) -> flatrev(Ls, []).

flatrev([C | Ls], Acc) when is_integer(C) -> flatrev(Ls, [C | Acc]);
flatrev([L | Ls], Acc) -> flatrev(Ls, flatrev(L, Acc));
flatrev([], Acc) -> Acc.


%% Count the number of elements in a nested list.
reclength([L | Ls]) when is_list(L) ->
    reclength(L) + reclength(Ls);
reclength([_ | Ls]) ->
    reclength(Ls) + 1;
reclength([]) -> 0.

%%% tags.erl ends here.
