Whitespace only
[ghc-hetmet.git] / utils / hasktags / HaskTags.hs
1 module Main where
2 import Char
3 import List
4 import IO
5 import System.Environment
6 import System.Console.GetOpt
7 import System.Exit
8
9
10 -- search for definitions of things
11 -- we do this by looking for the following patterns:
12 -- data XXX = ...      giving a datatype location
13 -- newtype XXX = ...   giving a newtype location
14 -- bla :: ...          giving a function location
15 --
16 -- by doing it this way, we avoid picking up local definitions
17 --              (whether this is good or not is a matter for debate)
18 --
19
20 -- We generate both CTAGS and ETAGS format tags files
21 -- The former is for use in most sensible editors, while EMACS uses ETAGS
22
23 --
24 -- TODO add tag categories
25 -- alternatives: http://haskell.org/haskellwiki/Tags
26
27 main :: IO ()
28 main = do
29         progName <- getProgName
30         args <- getArgs
31         let usageString = "Usage: " ++ progName ++ " [OPTION...] [files...]"
32         let (modes, filenames, errs) = getOpt Permute options args
33         if errs /= [] || elem Help modes || filenames == []
34          then do
35            putStr $ unlines errs
36            putStr $ usageInfo usageString options
37            exitWith (ExitFailure 1)
38          else return ()
39         let mode = getMode (Append `delete` modes)
40         let openFileMode = if elem Append modes
41                            then AppendMode
42                            else WriteMode
43         filedata <- mapM findthings filenames
44         if mode == BothTags || mode == CTags
45          then do
46            ctagsfile <- openFile "tags" openFileMode
47            writectagsfile ctagsfile filedata
48            hClose ctagsfile
49          else return ()
50         if mode == BothTags || mode == ETags
51          then do
52            etagsfile <- openFile "TAGS" openFileMode
53            writeetagsfile etagsfile filedata
54            hClose etagsfile
55          else return ()
56
57 -- | getMode takes a list of modes and extract the mode with the
58 --   highest precedence.  These are as follows: Both, CTags, ETags
59 --   The default case is Both.
60 getMode :: [Mode] -> Mode
61 getMode [] = BothTags
62 getMode [x] = x
63 getMode (x:xs) = max x (getMode xs)
64
65
66 data Mode = ETags | CTags | BothTags | Append | Help deriving (Ord, Eq, Show)
67
68 options :: [OptDescr Mode]
69 options = [ Option "c" ["ctags"]
70             (NoArg CTags) "generate CTAGS file (ctags)"
71           , Option "e" ["etags"]
72             (NoArg ETags) "generate ETAGS file (etags)"
73           , Option "b" ["both"]
74             (NoArg BothTags) ("generate both CTAGS and ETAGS")
75           , Option "a" ["append"]
76             (NoArg Append) ("append to existing CTAGS and/or ETAGS file(s)")
77           , Option "h" ["help"] (NoArg Help) "This help"
78           ]
79
80 type FileName = String
81
82 type ThingName = String
83
84 -- The position of a token or definition
85 data Pos = Pos
86                 FileName -- file name
87                 Int      -- line number
88                 Int      -- token number
89                 String   -- string that makes up that line
90     deriving (Show, Eq)
91
92 -- A definition we have found
93 data FoundThing = FoundThing ThingName Pos
94     deriving (Show, Eq)
95
96 -- Data we have obtained from a file
97 data FileData = FileData FileName [FoundThing]
98
99 data Token = Token String Pos
100     deriving Show
101
102
103 -- stuff for dealing with ctags output format
104
105 writectagsfile :: Handle -> [FileData] -> IO ()
106 writectagsfile ctagsfile filedata = do
107     let things = concat $ map getfoundthings filedata
108     mapM_ (\x -> hPutStrLn ctagsfile $ dumpthing x) (sortThings things)
109
110 sortThings = sortBy (\(FoundThing a _) (FoundThing b _) -> compare a b)
111
112 getfoundthings :: FileData -> [FoundThing]
113 getfoundthings (FileData filename things) = things
114
115 dumpthing :: FoundThing -> String
116 dumpthing (FoundThing name (Pos filename line _ _)) =
117     name ++ "\t" ++ filename ++ "\t" ++ (show $ line + 1)
118
119
120 -- stuff for dealing with etags output format
121
122 writeetagsfile :: Handle -> [FileData] -> IO ()
123 writeetagsfile etagsfile filedata = do
124     mapM_ (\x -> hPutStr etagsfile $ e_dumpfiledata x) filedata
125
126 e_dumpfiledata :: FileData -> String
127 e_dumpfiledata (FileData filename things) =
128     "\x0c\n" ++ filename ++ "," ++ (show thingslength) ++ "\n" ++ thingsdump
129     where thingsdump = concat $ map e_dumpthing things
130           thingslength = length thingsdump
131
132 e_dumpthing :: FoundThing -> String
133 e_dumpthing (FoundThing name (Pos filename line token fullline)) =
134     (concat $ take (token + 1) $ spacedwords fullline)
135  ++ "\x7f" ++ (show line) ++ "," ++ (show $ line+1) ++ "\n"
136
137
138 -- like "words", but keeping the whitespace, and so letting us build
139 -- accurate prefixes
140
141 spacedwords :: String -> [String]
142 spacedwords [] = []
143 spacedwords xs = (blanks ++ wordchars):(spacedwords rest2)
144     where (blanks,rest) = span Char.isSpace xs
145           (wordchars,rest2) = span (\x -> not $ Char.isSpace x) rest
146
147
148 -- Find the definitions in a file
149
150 findthings :: FileName -> IO FileData
151 findthings filename = do
152     text <- readFile filename
153     evaluate text -- forces evaluation of text
154                   -- too many files were being opened otherwise since
155                   -- readFile is lazy
156     let aslines = lines text
157     let wordlines = map mywords aslines
158     let noslcoms = map stripslcomments wordlines
159     let tokens = concat $ zipWith3 (withline filename) noslcoms aslines [0 ..]
160     -- there are some tokens with "" (don't know why yet) this filter fixes it
161     let tokens' = filter (\(Token s _ ) -> (not .  null) s ) tokens
162     let nocoms = stripblockcomments tokens'
163     -- using nub because getcons and findstuff are parsing parts of the file twice
164     return $ FileData filename $ nub $ findstuff nocoms
165   where evaluate [] = return ()
166         evaluate (c:cs) = c `seq` evaluate cs
167         -- my words is mainly copied from Data.List.
168         -- difference abc::def is split into three words instead of one.
169         -- We should really be lexing Haskell properly here rather
170         -- than using hacks like this. In the future we expect hasktags
171         -- to be replaced by something using the GHC API.
172         mywords :: String -> [String]
173         mywords (':':':':xs) = "::" : mywords xs
174         mywords s =  case dropWhile isSpace s of
175                          "" -> []
176                          s' -> w : mywords s''
177                              where (w, s'') = myBreak s'
178                                    myBreak [] = ([],[])
179                                    myBreak (':':':':xs) = ([], "::"++xs)
180                                    myBreak (' ':xs) = ([],xs);
181                                    myBreak (x:xs) = let (a,b) = myBreak xs
182                                                     in  (x:a,b)
183
184 -- Create tokens from words, by recording their line number
185 -- and which token they are through that line
186
187 withline :: FileName -> [String] -> String -> Int -> [Token]
188 withline filename words fullline i =
189     zipWith (\w t -> Token w (Pos filename i t fullline)) words $ [0 ..]
190
191 -- comments stripping
192
193 stripslcomments :: [String] -> [String]
194 stripslcomments ("--":xs) = []
195 stripslcomments (x:xs) = x : stripslcomments xs
196 stripslcomments [] = []
197
198 stripblockcomments :: [Token] -> [Token]
199 stripblockcomments ((Token "\\end{code}" _):xs) = afterlitend xs
200 stripblockcomments ((Token "{-" _):xs) = afterblockcomend xs
201 stripblockcomments (x:xs) = x:stripblockcomments xs
202 stripblockcomments [] = []
203
204 afterlitend2 :: [Token] -> [Token]
205 afterlitend2 (x:xs) = afterlitend xs
206 afterlitend2 [] = []
207
208 afterlitend :: [Token] -> [Token]
209 afterlitend ((Token "\\begin{code}" _):xs) = xs
210 afterlitend (x:xs) = afterlitend xs
211 afterlitend [] = []
212
213 afterblockcomend :: [Token] -> [Token]
214 afterblockcomend ((Token token _):xs)
215  | contains "-}" token = xs
216  | otherwise           = afterblockcomend xs
217 afterblockcomend [] = []
218
219
220 -- does one string contain another string
221
222 contains :: Eq a => [a] -> [a] -> Bool
223 contains sub full = any (isPrefixOf sub) $ tails full
224
225 ints :: Int -> [Int]
226 ints i = i:(ints $ i+1)
227
228
229 -- actually pick up definitions
230
231 findstuff :: [Token] -> [FoundThing]
232 findstuff ((Token "module" _):(Token name pos):xs) =
233     FoundThing name pos : (getcons xs) ++ (findstuff xs)
234 findstuff ((Token "data" _):(Token name pos):xs) =
235     FoundThing name pos : (getcons xs) ++ (findstuff xs)
236 findstuff ((Token "newtype" _):(Token name pos):xs) =
237     FoundThing name pos : findstuff xs
238 findstuff ((Token "type" _):(Token name pos):xs) =
239     FoundThing name pos : findstuff xs
240 findstuff ((Token "class" _):xs) = findClassName xs
241 findstuff ((Token name pos):(Token "::" _):xs) =
242     FoundThing name pos : findstuff xs
243 findstuff (x:xs) = findstuff xs
244 findstuff [] = []
245
246 findClassName :: [Token] -> [FoundThing]
247 findClassName []  = []
248 findClassName [Token n p]  = [FoundThing n p]
249 findClassName xs = (\((Token n pos):xs) -> FoundThing n pos : findstuff xs) . drop2 . dropParens 0 $ xs
250 dropParens n ((Token "(" _ ):xs) = dropParens (n+1) xs
251 dropParens 0 (x:xs) = x:xs
252 dropParens 1 ((Token ")" _ ):xs) = xs
253 dropParens n ((Token ")" _ ):xs) = dropParens (n-1) xs
254 dropParens n (x:xs) = dropParens n xs
255 -- dropsEverything till token "=>" (if it is on the same line as the
256 -- first token. if not return tokens)
257 drop2 tokens@(x@(Token _ (Pos _ line_nr _ _ )):xs) =
258   let (line, following) = span (\(Token s (Pos _ l _ _)) -> l == line_nr) tokens
259       (_, following_in_line) = span (\(Token n _) -> n /= "=>") line
260   in case following_in_line of
261           (Token "=>" _:xs) ->  xs ++ following
262           _ -> tokens
263 drop2 xs = xs
264
265 -- get the constructor definitions, knowing that a datatype has just started
266
267 getcons :: [Token] -> [FoundThing]
268 getcons ((Token "=" _):(Token name pos):xs) =
269     FoundThing name pos : getcons2 xs
270 getcons (x:xs) = getcons xs
271 getcons [] = []
272
273 getcons2 ((Token "=" _):xs) = []
274 getcons2 ((Token "|" _):(Token name pos):xs) =
275     FoundThing name pos : getcons2 xs
276 getcons2 (x:xs) = getcons2 xs
277 getcons2 [] = []
278