Skip to content

feat: add Outline sidebar panel with LSP document symbols#321

Open
tenox7 wants to merge 3 commits into
eugenioenko:mainfrom
tenox7:feat/outline-symbols
Open

feat: add Outline sidebar panel with LSP document symbols#321
tenox7 wants to merge 3 commits into
eugenioenko:mainfrom
tenox7:feat/outline-symbols

Conversation

@tenox7

@tenox7 tenox7 commented Jul 4, 2026

Copy link
Copy Markdown

Summary

Adds an Outline tab to the sidebar (after Changes) showing the symbol tree of the active file — functions, methods, types with fields, markdown sections — with click/enter to jump to the symbol.

│ Explore  Find  Outline  »  ⋮ ││ manager.go x │
│──────────────────────────────│┘              └──────────────────────
│▼ ◆ Manager                   │   1  package lsp
│   ▪ servers                  │   2
│   ▪ config                   │   3  import (
│   ▪ mu                       │   4    "fmt"
│   ▪ OnDiagnostics            │   5    "log/slog"
│ƒ NewManager                  │   6    "path/filepath"
│ƒ (*Manager).ServerConfig     │   7    "strings"
│ƒ (*Manager).ClientForLangu…  │   8    "sync"
  • LSP textDocument/documentSymbol support in internal/lsp: handles both hierarchical DocumentSymbol[] and flat SymbolInformation[] response shapes (flat is converted and position-sorted); advertises hierarchicalDocumentSymbolSupport.
  • Navigation model: arrowing through the tree reveals the symbol in the editor without stealing focus; enter/click on a leaf jumps and hands focus to the editor; the outline selection follows the editor cursor (nearest enclosing symbol), so it always shows where you are.
  • Built-in fallbacks when no server can answer: markdown ATX headings (fence-aware, nested by level) and a go/parser-based Go outline (functions, methods with receivers, structs/interfaces with members, consts/vars). Covers vendored files and module-less files where gopls answers no views, and machines without gopls entirely. The fallback renders immediately as a provisional outline and is replaced by the server response when it arrives.
  • Diagnostic empty states instead of a silent blank panel: " is not installed", "No language server for ", "Loading symbols…", and the actual server error text on failure.
  • didOpen fix: files opened as CLI args only announced the active tab to the server; tabs that become active without passing through OnFileOpen now get didOpen sent (fixes clangd -32602: trying to get AST for non-added document on those tabs, and heals hover/completion/diagnostics for them too). documentSymbol retries once to absorb server cold starts.
  • Single-file launches (ttt file.go) now root the language server at the file's directory instead of initializing with an empty rootURI.
  • Refresh triggers: panel activation, tab switch, save, external file reload. Command palette Show Outline, View menu entry. No keybinding (ctrl+k o is taken); palette/menu only.

Testing

  • Unit: protocol parsing (both response shapes), markdown heading parser (nesting, fences, edge cases), Go fallback parser (kinds, receivers, partial-parse tolerance), nearest-symbol selection.
  • E2E (tests/e2e/outline_test.go): fallback rendering, keyboard navigation + jump + focus handoff, click jump, empty state, cursor-follow.
  • Functional (tests/functional/outline.test.js): headings render, activate jumps and focuses (verified by typing a marker), nesting indentation, empty state.
  • Manually verified against gopls (hierarchical), clangd (multi-tab didOpen case), marksman-missing markdown fallback, vendored/orphaned Go files, and a real interactive PTY session.

Adds an Outline tab to the sidebar showing the symbol tree of the
active file (functions, methods, types with fields, markdown headings),
with click/enter to jump.

- LSP textDocument/documentSymbol support: hierarchical DocumentSymbol
  and flat SymbolInformation response shapes, hierarchical capability
  advertised at initialize
- Selection reveals the symbol in the editor without stealing focus;
  enter/click on a leaf jumps and focuses the editor; outline selection
  follows the editor cursor (nearest enclosing symbol)
- Built-in fallbacks when no server can answer: markdown ATX headings
  and a go/parser-based Go outline (covers vendored files, module-less
  files, and gopls 'no views' failures); fallback renders immediately
  as provisional and is replaced by the server response
- Diagnostic empty states: server not installed, no server for
  language, loading, and the actual server error text
- didOpen is now sent for tabs that become active without passing
  through OnFileOpen (files opened as CLI args), fixing clangd errors
  for those tabs; documentSymbol retries once to absorb cold starts
- Single-file launches root the server at the file's directory instead
  of an empty rootURI
- Refresh on panel activation, tab switch, save, and external reload

Unit tests for protocol parsing and both fallback parsers, e2e tests
for rendering/navigation/jump/empty states, functional tests for the
real binary.
@tenox7 tenox7 requested a review from eugenioenko as a code owner July 4, 2026 11:23
Copilot AI review requested due to automatic review settings July 4, 2026 11:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an Outline sidebar panel that shows document symbols for the active file, integrating LSP textDocument/documentSymbol with built-in fallbacks (Markdown headings + Go parser) and navigation behaviors (reveal on selection, jump on activate, cursor-follow selection). This fits into the app’s sidebar/navigation UX alongside Explore/Find/Changes.

Changes:

  • Add Outline panel UI + commands/menus and wire refresh triggers (panel activation, tab switch, save, external reload).
  • Add LSP protocol + client support for textDocument/documentSymbol, handling both DocumentSymbol[] and SymbolInformation[].
  • Add Markdown/Go symbol fallbacks plus unit, functional, and e2e tests for outline behavior.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/functional/outline.test.js Functional coverage for markdown outline rendering, nesting, jumping, and empty state.
tests/e2e/outline_test.go E2E coverage for fallback rendering, keyboard/mouse navigation, focus handoff, and cursor-follow selection.
internal/lsp/protocol.go Adds document symbol capability + symbol/documentSymbol protocol types.
internal/lsp/client.go Implements DocumentSymbols request and parsing for both response shapes.
internal/lsp/client_test.go Unit tests for parsing hierarchical/flat/empty documentSymbol responses.
internal/app/widgets.go Adds Outline panel to sidebar and stores it on App.
internal/app/watch.go Refreshes symbols after external file reload.
internal/app/symbols_panel.go New Outline panel widget + Markdown fallback symbol extraction.
internal/app/symbols_panel_test.go Unit tests for markdown parsing, node building, and nearest-selection logic.
internal/app/symbols_go.go New Go fallback outline via go/parser + AST walking.
internal/app/symbols_go_test.go Unit tests for Go fallback symbols and partial-parse tolerance.
internal/app/menus.go Adds Outline entry to the View menu.
internal/app/lsp_convert.go Adds SymbolsResult event payload type.
internal/app/eventloop.go Wires outline refresh on active-file changes and selection follow on cursor moves.
internal/app/commands_view.go Registers sidebar.outline (“Show Outline”) command.
internal/app/commands_editor.go Refreshes symbols on save.
internal/app/callbacks.go Adds outline “Refresh” more-menu item and reveal/jump callbacks.
internal/app/app.go Adds Symbols panel field to App.
internal/app/app_lsp.go Adds symbol refresh/request pipeline, didOpen healing, and single-file root workdir fallback.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/app/app_lsp.go
Comment on lines +642 to +648
// Show the built-in outline immediately when available; the server
// response replaces it. Otherwise indicate that a request is running.
if len(fallback) > 0 {
a.ApplySymbols(fallback)
} else if a.Symbols.Tree.ItemCount() == 0 {
a.Symbols.SetStatus("Loading symbols…")
}

@tenox7 tenox7 Jul 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cffcdf2: the loading state now replaces the outline whenever the request targets a different file than the displayed content; same-file refreshes keep the current outline to avoid flicker.

Comment on lines +190 to +196
inFence := false
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "```") || strings.HasPrefix(trimmed, "~~~") {
inFence = !inFence
continue
}

@tenox7 tenox7 Jul 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cffcdf2: fences now track their opening marker character, so ~~~ no longer closes a ``` block; added a regression test.

tenox7 added 2 commits July 4, 2026 04:31
- Clear stale outline when switching files: show the loading state
  whenever the request is for a different file than the displayed
  content; same-file refreshes keep the current outline to avoid
  flicker.
- Markdown fences now only close on the marker character that opened
  them, so a ~~~ line no longer terminates a ``` block and headings
  inside code blocks stay out of the outline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants