packages feed

fast-tags (empty) → 0.0.1

raw patch · 6 files changed

+522/−0 lines, 6 filesdep +basedep +containersdep +textsetup-changed

Dependencies added: base, containers, text

Files

+ LICENSE view
@@ -0,0 +1,31 @@+Copyright Evan Laforge 2011++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Evan Laforge nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+
+ README view
@@ -0,0 +1,13 @@+See vimrc for an example of how to keep the tags file up to date automatically.++Design decisions:++- Only top-level functions with type declarations are tagged.++Not supported:++- \ continuation in strings is not parsed correctly.++- Literate haskell.++- Infix operators.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ fast-tags.cabal view
@@ -0,0 +1,36 @@+name: fast-tags+version: 0.0.1+cabal-version: >= 1.6+build-type: Simple+synopsis: Fast incremental vi tags.+description:+    Yet another tags program.  Like hasktags, it uses its own parser rather+    than haskell-src or haskell-src-exts, so it's fast.  It should be even+    faster than hasktags because it uses Text instead of String.  It also+    understands hsc files, though not literate haskell.  It's also less buggy+    than hasktags.+    .+    In addition, it will load an existing tags file and merge generated tags.+    .+    The intent is to bind it to vim's BufWrite autocommand to automatically+    keep the tags file up to date.++category: Haskell, Development+license: BSD3+license-file: LICENSE+author: Evan Laforge+maintainer: Evan Laforge <qdunkan@gmail.com>+stability: experimental+tested-with: GHC>=7.0.3+data-files: README+extra-source-files: src/*.hs++source-repository head+    type: darcs+    location: http://ofb.net/~elaforge/darcs/fast-tags/++executable fast-tags+    main-is: Main.hs+    hs-source-dirs: src+    build-depends: base >= 3 && < 5, containers, text+    ghc-options: -Wall -fno-warn-name-shadowing
+ src/Main.hs view
@@ -0,0 +1,353 @@+{-# LANGUAGE OverloadedStrings, PatternGuards #-}+{- |+    Annotate lines, strip comments, tokenize, then search for+    It loads the existing tags, and updates it for the given file.  Then+    a SaveBuf action runs tags every time you save a file.+    Or extend lushtags, the question is complete parsing ok?  It means I can't+    do it on save, since I'd have to invoke the build system to de-hsc.+-}+module Main where+import qualified Control.Exception as Exception+import Control.Monad+import qualified Data.Char as Char+import qualified Data.Either as Either+import qualified Data.List as List+import qualified Data.Monoid as Monoid+import qualified Data.Text as T+import Data.Text (Text)+import qualified Data.Text.IO as Text.IO++import qualified System.Console.GetOpt as GetOpt+import qualified System.Environment as Environment+import qualified System.Exit+import qualified System.IO as IO+import qualified System.IO.Error as IO.Error+++main :: IO ()+main = do+    args <- Environment.getArgs+    (flags, inputs) <- case GetOpt.getOpt GetOpt.Permute options args of+        (flags, inputs, []) -> return (flags, inputs)+        (_, _, errs) -> usage $ "flag errors:\n" ++ List.intercalate ", " errs+    let output = last $ "tags" : [fn | Output fn <- flags]+    oldTags <- fmap (maybe [vimMagicLine] T.lines) $+        catchENOENT $ Text.IO.readFile output+    tags <- fmap concat (mapM processFile inputs)++    let (warnings, newTags) = Either.partitionEithers $ map showTag tags+    forM_ warnings $ \warn -> do+        IO.hPutStrLn IO.stderr warn+    let write = if output == "-" then Text.IO.hPutStr IO.stdout+            else Text.IO.writeFile output+    write $ T.unlines $+        merge (List.sort newTags) (filter (not . isNewTag inputs) oldTags)+    where+    usage msg = putStr (GetOpt.usageInfo msg options)+        >> System.Exit.exitSuccess++data Flag = Output FilePath+    deriving (Show)++options :: [GetOpt.OptDescr Flag]+options =+    [ GetOpt.Option ['o'] [] (GetOpt.ReqArg Output "blorch")+        "output file, defaults to 'tags'"+    ]++-- | Documented in vim :h tags-file-format.+-- This tells vim that the file is sorted (but not case folded) so that+-- it can do a bsearch and never needs to fall back to linear search.+vimMagicLine :: Text+vimMagicLine = "!_TAG_FILE_SORTED\t1\t~"++isNewTag :: [FilePath] -> Text -> Bool+isNewTag fns line = any (`T.isInfixOf` line) textFns+    where textFns = map (T.pack . ('\t':)) fns++merge :: [Text] -> [Text] -> [Text]+merge [] ys = ys+merge xs [] = xs+merge (x:xs) (y:ys)+    | x <= y = x : merge xs (y:ys)+    | otherwise = y : merge (x:xs) ys+++-- AbsoluteMark\tCmd/TimeStep.hs 67 ;" f+-- TODO use "file:" for non-exported symbols, this should make vim search for+-- them first within the file.+showTag :: Tag -> Either String Text+showTag (Pos (SrcPos fn lineno) (Tag text typ)) = Right $+    T.concat [text, "\t", T.pack fn, "\t", T.pack (show lineno), " ;\" ",+        T.singleton (showType typ)]+showTag (Pos pos (Warning text)) = Left $ show pos ++ ": " ++ text++showType :: Type -> Char+showType typ = case typ of+    Module -> 'm'+    Function -> 'f'+    Class -> 'c'+    Type -> 't'+    Constructor -> 'C'++-- * types++data TagVal = Tag !Text !Type | Warning !String+    deriving (Eq, Show)++data Type = Module | Function | Class | Type | Constructor+    deriving (Eq, Show)++data TokenVal = Token !Text | Newline !Int+    deriving (Eq, Show)++type Tag = Pos TagVal+type Token = Pos TokenVal+type Line = Pos Text++data Pos a = Pos {+    posOf :: SrcPos+    , valOf :: a+    }++data SrcPos = SrcPos {+    posFile :: !FilePath+    , posLine :: !Int+    }++instance (Show a) => Show (Pos a) where+    show (Pos pos val) = show pos ++ ":" ++ show val+instance Show SrcPos where+    show (SrcPos fn line) = fn ++ ":" ++ show line++processFile :: FilePath -> IO [Tag]+processFile fn = fmap (process fn) (Text.IO.readFile fn)++process :: FilePath -> Text -> [Tag]+process fn = dropDups tagText . concatMap blockTags . breakBlocks+    . stripComments . concatMap tokenize . stripCpp . annotate fn+    where+    tagText (Pos _ (Tag text _)) = text+    tagText (Pos _ (Warning warn)) = T.pack warn+++-- * tokenize++annotate :: FilePath -> Text -> [Line]+annotate fn text =+    [Pos (SrcPos fn num) line | (num, line) <- zip [1..] (T.lines text)]++-- | Also strips out hsc detritus.+stripCpp :: [Line] -> [Line]+stripCpp = filter $ not . ("#" `T.isPrefixOf`) . valOf++tokenize :: Line -> [Token]+tokenize (Pos pos line) = map (Pos pos) (tokenizeLine line)++tokenizeLine :: Text -> [TokenVal]+tokenizeLine text = Newline nspaces : go line+    where+    nspaces = T.count " " spaces + T.count "\t" spaces * 8+    (spaces, line) = T.break (not . Char.isSpace) text+    symbols = ["--", "{-", "-}", "=>", "->", "::"]+    go unstripped+        | T.null text = []+        | Just sym <- List.find (`T.isPrefixOf` text) symbols =+            Token sym : go (T.drop (T.length sym) text)+        | c == '\'' = let (token, rest) = breakChar text+            in Token (T.cons c token) : go rest+        | c == '"' = let (token, rest) = breakString cs+            in Token (T.cons c token) : go rest+        | Char.isSymbol c || Char.isPunctuation c =+            Token (T.singleton c) : go cs+        | otherwise =+            let (token, rest) = T.span isIdent text in Token token : go rest+        where+        text = T.dropWhile Char.isSpace unstripped+        c = T.head text+        cs = T.tail text+    isIdent c = Char.isAlphaNum c || c == '.' || c == '\'' || c == '_'++breakChar :: Text -> (Text, Text)+breakChar text+    | T.null text = ("", "")+    | T.head text == '\\' = T.splitAt 3 text+    | otherwise = T.splitAt 2 text++-- | Break after the ending double-quote of a string.+--+-- TODO \ continuation isn't supported.  I'd have to tokenize at the file+-- level instead of the line level.+breakString :: Text -> (Text, Text)+breakString text = case T.uncons post of+        Nothing -> (text, "")+        Just (c, cs)+            | c == '\\' && T.null cs -> (T.snoc pre c, "")+            | c == '\\' && T.head cs == '"' ->+                let (pre', post') = breakString (T.tail cs)+                in (pre <> "\\\"" <> pre', post')+            | c == '\\' ->+                let (pre', post') = breakString cs+                in (T.snoc pre c <> pre', post')+            | otherwise -> (T.snoc pre c, cs)+    where+    (pre, post) = T.break (\c -> c == '\\' || c == '"') text++stripComments :: [Token] -> [Token]+stripComments = go 0+    where+    go :: Int -> [Token] -> [Token]+    go _ [] = []+    go nest (pos@(Pos _ token) : rest)+        | token == Token "--" = go nest (dropLine rest)+        | token == Token "{-" = go (nest+1) rest+        | token == Token "-}" = go (nest-1) rest+        | nest > 0 = go nest rest+        | otherwise = pos : go nest rest+++-- | Break the input up into blocks based on indentation and strip out all+-- the newlines.  This way the blockTags doesn't need to worry about newlines.+breakBlocks :: [Token] -> [[Token]]+breakBlocks =+    filter (not . null) . map (filter (not . isNewline)) . go . filterBlank+    where+    go [] = []+    go tokens = pre : breakBlocks post+        -- Start the indent off at 1, this way the next line in the 0th column+        -- will start a new block.+        where (pre, post) = breakBlock True 1 tokens+    -- Blank lines mess up the indentation.+    filterBlank [] = []+    filterBlank (Pos _ (Newline _) : xs@(Pos _ (Newline _) : _)) =+        filterBlank xs+    filterBlank (x:xs) = x : filterBlank xs++-- | Take until newline, then take lines until there the indent decreases.+breakBlock :: Bool -> Int -> [Token] -> ([Token], [Token])+breakBlock _ _ [] = ([], [])+breakBlock firstLine indent (t@(Pos _ (Newline lineIndent)) : ts)+    -- An indent less than the established block indent means I'm done.+    | lineIndent < indent = ([t], ts)+    | otherwise =+        let (pre, post) = breakBlock False blockIndent ts in (t:pre, post)+    where blockIndent = if firstLine then lineIndent else indent+breakBlock firstLine indent (t : ts) =+    let (pre, post) = breakBlock firstLine indent ts in (t:pre, post)+++-- * extract tags++-- | Get all the tags in one indented block.+--+-- Search for:+-- - module <word>+-- - newtype <word>+-- - type <word>+-- - data <word> * = <word> :: or | <word>+-- - class * => <word> * where - strip indent, look for <word> ::+-- - <word> :: emit as a definition+-- - whitespace: skip to next line+blockTags :: [Token] -> [Tag]+blockTags tokens = case tokens of+    [] -> []+    Pos _ (Token "module") : Pos pos (Token name) : _ ->+        [mktag pos (snd (T.breakOnEnd "." name)) Module]+    -- newtype X * = X *+    Pos _ (Token "newtype") : Pos pos (Token name) : rest ->+        mktag pos name Type : newtypeTags pos rest+    -- type X * = ...+    Pos _ (Token "type") : Pos pos (Token name) : _ -> [mktag pos name Type]+    -- data X * = X { X :: *, X :: * }+    -- data X * where ...+    Pos _ (Token "data") : Pos pos (Token name) : rest ->+        mktag pos name Type : dataTags pos rest+    -- class * => X where X :: * ...+    Pos pos (Token "class") : rest -> classTags pos rest+    Pos pos (Token name) : Pos _ (Token "::") : _ -> [mktag pos name Function]+    _ -> []++mktag :: SrcPos -> Text -> Type -> Pos TagVal+mktag pos name typ = Pos pos (Tag name typ)++warning :: SrcPos -> String -> Pos TagVal+warning pos warn = Pos pos (Warning warn)++unexpected :: SrcPos -> [Token] -> [Token] -> String -> [Tag]+unexpected prevPos tokens tokensHere declaration =+    [warning pos ("unexpected " ++ thing ++ " after " ++ declaration)]+    where+    thing = if null tokensHere then "end of block"+        else show (valOf (head tokensHere))+    pos+        | not (null tokensHere) = posOf (head tokensHere)+        | not (null tokens) = posOf (last tokens)+        | otherwise = prevPos++-- | * = X *+newtypeTags :: SrcPos -> [Token] -> [Tag]+newtypeTags prevPos tokens = case dropUntil "=" tokens of+    Pos pos (Token name) : _ -> [mktag pos name Constructor]+    rest -> unexpected prevPos tokens rest "newtype * ="++-- | [] (empty data declaration)+-- * = X { X :: *, X :: * }+-- * where X :: * X :: *+-- * = X | X+dataTags :: SrcPos -> [Token] -> [Tag]+dataTags _ [] = [] -- empty data declaration+dataTags prevPos tokens+    -- if is gadt then...+    | otherwise = case dropUntil "=" tokens of+        Pos pos (Token name) : rest ->+            mktag pos name Constructor : collectRest rest+        rest -> unexpected prevPos tokens rest "data * ="+    where+    collectRest tokens = case tokens of+        Pos pos (Token name) : Pos _ (Token "::") : rest ->+            mktag pos name Function : collectRest rest+        Pos _ (Token "|") : Pos pos (Token name) : rest ->+            mktag pos name Constructor : collectRest rest+        _ : rest -> collectRest rest+        [] -> []++-- | * => X where X :: * ...+classTags :: SrcPos -> [Token] -> [Tag]+classTags prevPos unstripped = case tokens of+    Pos pos (Token name) : rest ->+        mktag pos name Class : collectRest rest+    rest -> unexpected prevPos tokens rest "class * =>"+    where+    tokens = if any ((== Token "=>") . valOf) unstripped+        then dropUntil "=>" unstripped else unstripped+    collectRest tokens = case tokens of+        Pos pos (Token name) : Pos _ (Token "::") : rest ->+            mktag pos name Function : collectRest rest+        _ : rest -> collectRest rest+        [] -> []++dropLine :: [Token] -> [Token]+dropLine = drop 1 . dropWhile (not . isNewline)++isNewline :: Token -> Bool+isNewline (Pos _ (Newline _)) = True+isNewline _ = False++dropUntil :: Text -> [Token] -> [Token]+dropUntil token = drop 1 . dropWhile ((/= Token token) . valOf)+++-- * misc++(<>) :: (Monoid.Monoid a) => a -> a -> a+(<>) = Monoid.mappend++-- | If @op@ raised ENOENT, return Nothing.+catchENOENT :: IO a -> IO (Maybe a)+catchENOENT op = Exception.handleJust (guard . IO.Error.isDoesNotExistError)+    (const (return Nothing)) (fmap Just op)++dropDups :: (Eq k) => (a -> k) -> [a] -> [a]+dropDups _ [] = []+dropDups key (x:xs) = x : map snd (filter (not . equal) (zip (x:xs) xs))+    where equal (x, y) = key x == key y
+ src/Main_test.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE OverloadedStrings #-}+module Main_test where+import Control.Monad+import qualified Data.Maybe as Maybe+import qualified Data.Text as Text++import qualified Main as Main+import Main (Token, TokenVal(..), TagVal(..), Type(..))+++-- This is kind of annoying without line numbers on equal and automatic+-- test_* collection...+main = do+    test_tokenize+    test_breakString+    test_stripComments+    test_process++test_tokenize = do+    let f = extractTokens . tokenize+    equal (f "a::b->c") ["nl 0", "a", "::", "b", "->", "c"]+    equal (f "x{-\n  bc#-}\n")+        ["nl 0", "x", "{-", "nl 2", "bc", "#", "-}"]+    equal (f "X.Y") ["nl 0", "X.Y"]++test_breakString = do+    let f = Main.breakString+    equal (f "hi \" there") ("hi \"", " there")+    equal (f "hi \\a \" there") ("hi \\a \"", " there")+    equal (f "hi \\\" there\"") ("hi \\\" there\"", "")+    equal (f "hi") ("hi", "")+    -- String continuation isn't implemented yet.+    equal (f "hi \\") ("hi \\", "")++test_stripComments = do+    let f = extractTokens . Main.stripComments . tokenize+    equal (f "hello -- there") ["nl 0", "hello"]+    equal (f "hello {- there -} fred") ["nl 0", "hello", "fred"]+    equal (f "{-# LANG #-} hello {- there {- nested -} comment -} fred")+        ["nl 0", "hello", "fred"]++test_breakBlocks = do+    let f = map extractTokens . Main.breakBlocks . tokenize+    equal (f "1\n2\n") [["1"], ["2"]]+    equal (f "1\n 1\n2\n") [["1", "1"], ["2"]]+    equal (f "1\n 1\n 1\n2\n") [["1", "1", "1"], ["2"]]+    -- intervening blank lines are ignored+    equal (f "1\n 1\n\n 1\n2\n") [["1", "1", "1"], ["2"]]+    equal (f "1\n\n\n 1\n2\n") [["1", "1"], ["2"]]++test_process = do+    let f = map Main.valOf . Main.process "fn"+    equal (f "module Bar.Foo where\n") [Tag "Foo" Module]+    equal (f "newtype Foo a b =\n\tBar x y z\n")+        [Tag "Foo" Type, Tag "Bar" Constructor]+    equal (f "data Foo a = Bar { field :: Field }")+        [Tag "Foo" Type, Tag "Bar" Constructor, Tag "field" Function]+    equal (f "data Foo = Bar | Baz")+        [Tag "Foo" Type, Tag "Bar" Constructor, Tag "Baz" Constructor]+    equal (f "class (X x) => C a b c where\n\tm :: a -> b\n\tn :: c -> d\n")+        [Tag "C" Class, Tag "m" Function, Tag "n" Function]+    equal (f "class A a where f :: X\n")+        [Tag "A" Class, Tag "f" Function]+    equal (f "data X\n") [Tag "X" Type]++    -- The extra X is suppressed.+    equal (f "data X = X Int\n") [Tag "X" Type]++    equal (f "f :: A -> B\ng :: C -> D\ndata D = C {\n\tf :: A\n\t}\n")+        [Tag "f" Function, Tag "g" Function, Tag "D" Type,+            Tag "C" Constructor, Tag "f" Function]+++tokenize :: Text.Text -> [Token]+tokenize = concat . map Main.tokenize . Main.stripCpp . Main.annotate "fn"++plist :: (Show a) => [a] -> IO ()+plist xs = mapM_ (putStrLn . show) xs >> putChar '\n'++extractTokens :: [Token] -> [Text.Text]+extractTokens = Maybe.mapMaybe $ \token -> case Main.valOf token of+    Token name -> Just name+    Newline n -> Just (Text.pack ("nl " ++ show n))++equal :: (Show a, Eq a) => a -> a -> IO ()+equal x y = unless (x == y) $+    putStrLn $ "__: " ++ show x ++ " /= " ++ show y