diff --git a/README.md b/README.md index 4b18966..b7d56f4 100644 --- a/README.md +++ b/README.md @@ -244,9 +244,14 @@ Display an entire directory: llmcat . ``` -Produce a map of a directory or file: +Display a map of a repo or file: ```bash -llmcat --outline llmcat.go +llmcat --outline . +``` + +Display a map of the repo, but with the `RenderDirectory` function expanded: +```bash +llmcat --outline --expand "llmcat.go RenderDirectory" . ``` ### Navigation diff --git a/cmd/llmcat/main.go b/cmd/llmcat/main.go index df5295a..17b327c 100644 --- a/cmd/llmcat/main.go +++ b/cmd/llmcat/main.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/everestmz/llmcat" + "github.com/everestmz/llmcat/ctxspec" "github.com/rs/zerolog" "github.com/spf13/cobra" ) @@ -21,6 +22,18 @@ func main() { RunE: func(cmd *cobra.Command, args []string) error { path := args[0] + contextSpecLines, err := cmd.Flags().GetStringSlice("expand") + if err != nil { + return err + } + + contextSpec, err := ctxspec.ParseContextSpec(strings.Join(contextSpecLines, "\n")) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + dirOptions.ContextSpec = contextSpec + dirOptions.FileOptions = &options if strings.HasSuffix(path, ".git") { @@ -73,6 +86,7 @@ func main() { flags.BoolVarP(&options.ShowLineNumbers, "line-numbers", "n", true, "show line numbers") flags.StringVarP(&options.GutterSeparator, "separator", "s", "|", "gutter separator character") flags.BoolVar(&options.Outline, "outline", false, "produce an outline for supported source files using tree-sitter") + flags.StringArrayVar(&options.ExpandSymbols, "symbols", nil, "specify symbols to expand when showing an outline") // Pagination flags flags.IntVarP(&options.PageSize, "page-size", "p", 10000, "number of lines to show (0 = show all)") @@ -84,7 +98,8 @@ func main() { flags.StringSliceVar(&dirOptions.IncludeGlobs, "include", nil, "glob patterns to include") flags.StringSliceVar(&dirOptions.ExcludeExtensions, "exclude-ext", nil, "comma-separated list of file extensions to exclude") flags.StringSliceVar(&dirOptions.IncludeExtensions, "ext", nil, "comma-separated list of file extensions to include") - // flags.BoolVarP(&dirOptions.ShowTree, "tree", "t", false, "show directory tree") + + flags.StringSliceP("expand", "e", nil, "symbols or files to expand when showing an outline, in ctxspec format") if err := rootCmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) diff --git a/llmcat.go b/llmcat.go index a3b7ab5..64d7c9e 100644 --- a/llmcat.go +++ b/llmcat.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" + "github.com/everestmz/llmcat/ctxspec" "github.com/everestmz/llmcat/treesym" "github.com/everestmz/llmcat/treesym/language" "github.com/gobwas/glob" @@ -15,13 +16,22 @@ import ( ) type RenderFileOptions struct { - Outline bool `json:"outline"` - OutputMarkdown bool `json:"output_markdown"` - ShowLineNumbers bool `json:"hide_line_numbers"` - GutterSeparator string `json:"gutter_separator"` - PageSize int `json:"page_size"` - StartLine int `json:"start_line"` - ShowPageInfo bool `json:"show_page_info"` + Outline bool `json:"outline"` + OutputMarkdown bool `json:"output_markdown"` + ShowLineNumbers bool `json:"hide_line_numbers"` + GutterSeparator string `json:"gutter_separator"` + PageSize int `json:"page_size"` + StartLine int `json:"start_line"` + ShowPageInfo bool `json:"show_page_info"` + ExpandSymbols []string `json:"expand_symbols"` +} + +// TODO: split this up so we produce another type which contains +// ExpandSymbols - they're not a generic input, they're specific to a file +func (ro *RenderFileOptions) Copy() *RenderFileOptions { + new := *ro + + return &new } func (ro *RenderFileOptions) SetDefaults() { @@ -94,6 +104,16 @@ func RenderFile(filename, text string, options *RenderFileOptions) (string, erro return line } + shouldExpandChunk := func(chunk *treesym.OutlineChunk) bool { + for _, symbolName := range options.ExpandSymbols { + if chunk.Name == symbolName { + return true + } + } + + return false + } + chunks, err := treesym.GetSymbols(context.TODO(), &treesym.SourceFile{ Path: filename, Text: text, @@ -122,7 +142,7 @@ func RenderFile(filename, text string, options *RenderFileOptions) (string, erro // This chunk is at least partially in the range - if options.Outline && chunk.ShouldOmit { + if options.Outline && chunk.ShouldOmit && !shouldExpandChunk(chunk) { // Specify how many lines have been omitted (it may not be the size of the chunk, // if some of it is on the next or previous page!) var headLinesAlreadyOmitted, tailLinesAlreadyOmitted int @@ -169,11 +189,12 @@ func RenderFile(filename, text string, options *RenderFileOptions) (string, erro // We should probably allow for glob-based ignores, extension-based ignores, and some other dir-based filters type RenderDirectoryOptions struct { - FileOptions *RenderFileOptions `json:"file_options"` - IgnoreGlobs []string `json:"ignore_globs"` - IncludeGlobs []string `json:"include_globs"` - IncludeExtensions []string `json:"include_extensions"` - ExcludeExtensions []string `json:"exclude_extensions"` + FileOptions *RenderFileOptions `json:"file_options"` + IgnoreGlobs []string `json:"ignore_globs"` + IncludeGlobs []string `json:"include_globs"` + IncludeExtensions []string `json:"include_extensions"` + ExcludeExtensions []string `json:"exclude_extensions"` + ContextSpec ctxspec.ContextSpec `json:"context_spec"` compiledIgnoreGlobs []glob.Glob compiledIncludeGlobs []glob.Glob @@ -301,7 +322,17 @@ func RenderDirectory(dirName string, options *RenderDirectoryOptions) (string, e return err } - rendered, err := RenderFile(relPath, string(text), options.FileOptions) + fileOpts := options.FileOptions + if spec, ok := options.ContextSpec[relPath]; ok { + fileOpts = fileOpts.Copy() + if len(spec.Symbols) > 0 { + fileOpts.ExpandSymbols = spec.Symbols + } else { + // Just show everything + fileOpts.Outline = false + } + } + rendered, err := RenderFile(relPath, string(text), fileOpts) if err != nil { return err } diff --git a/treesym/treesym.go b/treesym/treesym.go index dc97850..5107586 100644 --- a/treesym/treesym.go +++ b/treesym/treesym.go @@ -32,6 +32,8 @@ type Symbols struct { } type OutlineChunk struct { + // Name only set if item is omitted, since otherwise chunk could be bigger than a single symbol + Name string Content string ShouldOmit bool // 0-indexed, like tree-sitter rows are @@ -85,6 +87,7 @@ func (psf *ProcessedSourceFile) GetOutline() []*OutlineChunk { ShouldOmit: true, StartRow: startLine, EndRow: endLine, + Name: def.Name, } for currentLine := startLine; currentLine <= endLine; currentLine++ {