Skip to content

fix(ui): stabilize tab switching, gutter layering, folder expansion, and lsp underlines - #23

Merged
Yan123-tech merged 9 commits into
mainfrom
fix/ui-stability-and-diagnostics
Oct 12, 2026
Merged

Yan123-tech merged 9 commits into
mainfrom
fix/ui-stability-and-diagnostics

Conversation

@Yan123-tech

Copy link
Copy Markdown
Owner

Summary

This PR addresses critical UI stability and editor presentation issues discovered during self-testing:

  1. Tab Fluctuation & Code Editor Flickering: Rapid tab switching/oscillation and editor reloads caused by cyclic event cascades between active document changes and sidebar selection.
  2. LSP Diagnostic Underlining: Diagnostics not highlighting compiler errors in the code editor due to Tree-sitter highlighter overwriting NSTextStorage attributes.
  3. Line Number Gutter Layering: Gutter line numbers bleeding and drawing across the tab bar during editor scrolling.
  4. Folder Expansion Double-Toggle: Folders failing to expand on the first click without selecting the row first.

Detailed Changes

1. Fix Tab Fluctuation & Editor Flickering

  • Root Cause: FileExplorerView's .task(id: activeDocumentURL) called revealActiveDocument(), mutating selection. This triggered .onChange(of: selection) which called onOpenFile(item.url), re-activating the document asynchronously and creating a cyclical feedback loop that repeatedly reloaded the editor.
  • Fix:
    • Added programmatic suppression flag isSyncingSelection in FileExplorerView to guard .onChange(of: selection).
    • Guarded activate(_:) in FileExplorerView from re-opening items when item.url == activeDocumentURL.
    • Added guard activeDocument !== document else { return } in DocumentManager.activate(_:).

2. Fix LSP Diagnostic Underlining in Code Editor Area

  • Root Cause: Diagnostic underlines were being added as attributes on NSTextStorage. CodeEditTextView CoreText layout does not render standard TextKit underline attributes, and CodeEditSourceEditor's Tree-sitter Highlighter wipes raw storage attributes on every pass.
  • Fix:
    • Migrated diagnostic rendering in LSPTextViewCoordinator to CodeEditTextView's native EmphasisManager (textView.emphasisManager?.replaceEmphases(...)), drawing stable CAShapeLayer underlines with semantic theme colors.
    • Clamped zero-length point diagnostics (caret/token errors) to 1 character so underlines are visible.
    • Subscribed to cached server diagnostics on initial document attach and added robust URL canonicalization matching.
    • Added publishDiagnostics client capability in LSPTypes.swift and made Diagnostic decoding fault-tolerant against arbitrary server fields.

3. Fix Line Number Gutter Layering on Scroll

  • Root Cause: GutterView in CodeEditSourceEditor is an NSScrollView floating subview. When scrolling, AppKit subviews rendered over sibling SwiftUI views because editorArea lacked clipping and TabBarView had default z-index.
  • Fix:
    • Added .zIndex(1) and .clipped() to TabBarView.
    • Added explicit .clipped() to editorArea and CodeEditorView in MainWindowView.swift to ensure all overscroll and floating views remain masked to editor bounds.

4. Fix Folder Expansion Requiring Selection

  • Root Cause: The disclosure chevron button did selection = item; fileSystemManager.toggleExpansion(of: item). For unselected folders, setting selection triggered .onChange(of: selection) -> activate() -> toggleExpansion() a second time, immediately reversing the toggle.
  • Fix:
    • Removed selection = item from the chevron disclosure control in FileExplorerView.
    • Added arrow key navigation (.rightArrow to expand, .leftArrow to collapse).

Documentation

  • Documented findings, root causes, and architecture rules in docs/MEMORY.md (entries #24, #25, #26, #27).

Verification

  • swift test: All 115 unit & integration tests in 32 suites passed.
  • ./Scripts/build-app.sh: Release .app built and ad-hoc signed cleanly (106MB).

yan added 9 commits October 12, 2026 15:53
…ge-detection

Both features are merged and passing 113 tests. Updates CURRENT_STATE.md
to mark completion as wired and external-change detection as done, adds
MEMORY.md entries #19-#22 (language identifier matching, sourcekit string
IDs, UTF-16 positions, URL resourceValues quirks), and updates PROJECT_PLAN.md
quality gates, README roadmap, and PROCESS.md test counts.
Closes #2 (diagnostics part): sourcekit-lsp's publishDiagnostics notifications
now drive underlines on the text view and a live error/warning count in the
status bar.

- DocumentDiagnostic + DocumentDiagnosticsSeverity in Core (Foundation-only,
  so Document stores diagnostics without importing CodeEditorLSP)
- Document.setDiagnostics(_:): replaces the batch, @observable so the status
  bar re-renders
- LSPTextViewCoordinator subscribes to client.onDiagnosticsChanged in
  openIfNeeded(), converts LSP Diagnostic → DocumentDiagnostic via
  offset(forLine:column:), and paints .underlineStyle + .underlineColor
  from Theme.semantic (red/green/yellow)
- Clear + re-render on tab switch and destroy
- StatusBarView shows "N errors, M warnings" when diagnostics are present
- DocumentDiagnosticsTests: setDiagnostics round-trip + range-to-NSRange

Gutter markers are a follow-up — the upstream GutterView has no marker API in 0.15.
Closes #3: ⌘-click navigates to a symbol's definition, and the popover
shows server-provided documentation alongside the target.

- LSPTextViewCoordinator conforms to JumpToDefinitionDelegate
- queryLinks(forRange:): resolves the NSRange to an LSP Position, then
  fetches definition + hover concurrently via async let
- link(from:hover:): same-file links carry an NSRange so the upstream
  model sets the selection; cross-file links carry a URL the AppState
  opens
- CodeEditorView passes lspCoordinator as jumpToDefinitionDelegate
  to SourceEditor
A Settings sheet (⌘,) consolidates every persisted editor preference in one
Form so they're discoverable without hunting menu submenus.

- AppState gains isSettingsVisible (mirrors the isSidebar/isTerminal/isStatusBar
  pattern)
- SettingsView: theme picker (Menu with checkmark), font size, tab width,
  and the four view toggles — all bound to EditorState setters
- MainWindowView shows the sheet
- CodeEditorApp adds the Settings… command in the .appSettings group
- Theme, Chrome, and Semantic conform to Hashable (their only stored
  properties are already Hashable EditorColor values) so Theme can serve as
  a Picker/Selection value
…r/goto-def/settings

- CURRENT_STATE.md: 113→115 tests; diagnostics + hover/def + settings marked
  working; status bar diagnostic-count mention; size table bumped for 5 new files
  (Diagnostic.swift, DocumentDiagnosticsTests, LSPTextViewCoordinator edits,
  SettingsView, StatusBarView edits)
- PROJECT_PLAN.md: #2, #3, #4 marked done; test-count gate updated
- MEMORY.md: #23 on Picker(selection:) resolving to init(sources:) on this
  toolchain; FAQ entries for settings picker + diagnostic colours

Closes the 'before you call it done' checklist for issues #2, #3, #4.
…d lsp underlines

- fix(sidebar): prevent cyclic tab/selection oscillation by guarding programmatic reveals with isSyncingSelection and checking activeDocumentURL
- fix(sidebar): fix folder expansion requiring selection by decoupling disclosure chevron button from List selection state
- fix(chrome): prevent gutter line number layering over TabBarView by adding zIndex(1) and clipped()
- fix(lsp): switch diagnostic rendering from NSTextStorage attributes to CodeEditTextView EmphasisManager to prevent Tree-sitter wipes
- fix(lsp): add publishDiagnostics to ClientCapabilities and enable cached diagnostic rendering on document attach
- docs: document EmphasisManager, disclosure button, selection guard, and AppKit layering in MEMORY.md (#24-#27)
…d recent projects welcome hub

- Fix diagnostic highlight accuracy by aligning offset calculations to UTF-16 code units (LSP spec)
- Add diagnostic reason popover tooltip on cursor/mouse hover over error lines
- Disable aggressive session restoration on launch to prevent startup tab switching and editor flickers
- Add Welcome Hub in main window area featuring Recent Projects store with 1-click open, Middle-truncation, and Finder actions
- Add gopls integration with staticcheck, analyses, and unimported completions
- Add gofmt formatting on save and via Shift+Option+F shortcut
- Add Product menu commands for Run (Cmd+R) and Test (Cmd+U) in integrated terminal drawer
- Inject Go and local toolchain paths into GUI subprocess PATH environment
…d live diagnostics

- Synchronously sync document edits in DocumentTextCoordinator to guarantee immediate tab dirty indicator
- Extract TabItemView with @bindable document observation in TabBarView
- Add .id(document.id) to CodeEditorView to avoid stale tab coordinators and bindings
- Support trigger characters and range-based offset resolution for LSP code suggestions
- Dispatch LSP onDiagnosticsChanged to MainActor for real-time continuous linting and underlining
@Yan123-tech
Yan123-tech force-pushed the fix/ui-stability-and-diagnostics branch from f976e02 to 88a0c5f Compare October 12, 2026 12:53
@Yan123-tech
Yan123-tech merged commit 76a056e into main Oct 12, 2026
2 checks passed
Yan123-tech pushed a commit that referenced this pull request Oct 12, 2026
…r/goto-def/settings

- CURRENT_STATE.md: 113→115 tests; diagnostics + hover/def + settings marked
  working; status bar diagnostic-count mention; size table bumped for 5 new files
  (Diagnostic.swift, DocumentDiagnosticsTests, LSPTextViewCoordinator edits,
  SettingsView, StatusBarView edits)
- PROJECT_PLAN.md: #2, #3, #4 marked done; test-count gate updated
- MEMORY.md: #23 on Picker(selection:) resolving to init(sources:) on this
  toolchain; FAQ entries for settings picker + diagnostic colours

Closes the 'before you call it done' checklist for issues #2, #3, #4.
@Yan123-tech
Yan123-tech deleted the fix/ui-stability-and-diagnostics branch October 12, 2026 12:54
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.

1 participant