Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add an internal debounce to setOutline + move code to OutlineView #118

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 14 additions & 35 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,25 @@ import { notifyError, largeness as editorLargeness } from "atom-ide-base/commons
import { isItemVisible } from "atom-ide-base/commons-ui"

export { statuses } from "./statuses" // for spec
import { statuses } from "./statuses"
import debounce from "lodash/debounce"

const subscriptions = new CompositeDisposable()

let view: OutlineView | undefined
export const outlineProviderRegistry = new ProviderRegistry<OutlineProvider>()

// let busySignalProvider: BusySignalProvider | undefined // service might be consumed late
// export let busySignalProvider: BusySignalProvider | undefined // service might be consumed late

export function activate() {
addCommands()
addObservers()
if (atom.config.get("atom-ide-outline.initialDisplay")) {
// initially show outline pane
toggleOutlineView().catch((e) => {
try {
toggleOutlineView()
} catch (e) {
notifyError(e)
})
}
}
}

Expand Down Expand Up @@ -51,7 +52,7 @@ export function deactivate() {
// subscriptions.add(busySignalProvider)
// }

export async function consumeOutlineProvider(provider: OutlineProvider) {
export function consumeOutlineProvider(provider: OutlineProvider) {
subscriptions.add(/* providerRegistryEntry */ outlineProviderRegistry.addProvider(provider))

// NOTE Generate (try) an outline after obtaining a provider for the current active editor
Expand All @@ -60,13 +61,13 @@ export async function consumeOutlineProvider(provider: OutlineProvider) {
// or if the editor changes later once outline is visible
// so we need to have an outline for the current editor
// the following updates rely on the visibility
await getOutline()
getOutline()
}

// disposables returned inside onEditorChangedDisposable
let onEditorChangedDisposable: CompositeDisposable | undefined = undefined

async function editorChanged(editor?: TextEditor) {
function editorChanged(editor?: TextEditor) {
if (editor === undefined) {
return
}
Expand All @@ -78,7 +79,7 @@ async function editorChanged(editor?: TextEditor) {
// this is because we can't track if the outline tab becomes visible suddenly,
// so we always need to show the outline for the correct file
// the following updates rely on the visibility
await getOutline(editor)
getOutline(editor)

const largeness = editorLargeness(editor as TextEditor)
// How long to wait for the new changes before updating the outline.
Expand All @@ -98,7 +99,7 @@ async function editorChanged(editor?: TextEditor) {

// clean up if the editor editor is closed
editor.onDidDestroy(() => {
setStatus("noEditor")
view?.setStatus("noEditor")
})
)
}
Expand All @@ -115,7 +116,7 @@ export function revealCursor() {
}
}

export async function toggleOutlineView() {
export function toggleOutlineView() {
if (view === undefined) {
view = new OutlineView() // create outline pane
}
Expand All @@ -135,7 +136,7 @@ export async function toggleOutlineView() {

// Trigger an editor change whenever an outline is toggeled.
try {
await editorChanged(atom.workspace.getActiveTextEditor())
editorChanged(atom.workspace.getActiveTextEditor())
} catch (e) {
notifyError(e)
}
Expand All @@ -149,33 +150,11 @@ function getOutlintIfVisible(editor = atom.workspace.getActiveTextEditor()) {
return getOutline(editor)
}

export async function getOutline(editor = atom.workspace.getActiveTextEditor()) {
export function getOutline(editor = atom.workspace.getActiveTextEditor()) {
if (view === undefined) {
view = new OutlineView() // create outline pane
}
// editor
if (editor === undefined) {
return setStatus("noEditor")
}

// provider
const provider = outlineProviderRegistry.getProviderForEditor(editor)

if (!provider) {
return setStatus("noProvider")
}

// const busySignalID = `Outline: ${editor.getPath()}`
// busySignalProvider?.add(busySignalID)

const outline = await provider.getOutline(editor)
view.setOutline(outline?.outlineTrees ?? [], editor, Boolean(editorLargeness(editor as TextEditor)))

// busySignalProvider?.remove(busySignalID)
}

export function setStatus(id: "noEditor" | "noProvider") {
view?.presentStatus(statuses[id])
view.setOutline(editor)
}

export { default as config } from "./config.json"
46 changes: 45 additions & 1 deletion src/outlineView.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { TextEditor, Point } from "atom"
import { OutlineTree } from "atom-ide-base"
import { isItemVisible, scrollIntoViewIfNeeded } from "atom-ide-base/commons-ui"
import { statuses } from "./statuses"
import { outlineProviderRegistry } from "./main"
import { largeness as editorLargeness } from "atom-ide-base/commons-atom"

export class OutlineView {
public element: HTMLDivElement
Expand All @@ -17,6 +20,9 @@ export class OutlineView {
/** cache of last rendered list used to avoid rerendering */
lastEntries: OutlineTree[] | undefined

private setOutlineTimeout: NodeJS.Timeout | undefined
private isRendering: boolean = false

constructor() {
this.element = document.createElement("div")
this.element.classList.add("atom-ide-outline")
Expand Down Expand Up @@ -45,7 +51,38 @@ export class OutlineView {
return "list-unordered"
}

setOutline(outlineTree: OutlineTree[], editor: TextEditor, isLarge: boolean) {
setOutline(editor: TextEditor | undefined) {
if (this.isRendering) {
// return if setOutline is already called and not finished yet.
return
}
this.isRendering = true
// const busySignalID = `Outline: ${editor.getPath()}`
// busySignalProvider?.add(busySignalID)

// editor
if (editor === undefined) {
return this.setStatus("noEditor")
}

// provider
const provider = outlineProviderRegistry.getProviderForEditor(editor)

if (!provider) {
return this.setStatus("noProvider")
}

if (this.setOutlineTimeout) {
clearTimeout(this.setOutlineTimeout)
}
this.setOutlineTimeout = setTimeout(async () => {
const outline = await provider.getOutline(editor)
this._setOutline(outline?.outlineTrees ?? [], editor, Boolean(editorLargeness(editor as TextEditor)))
// busySignalProvider?.remove(busySignalID)
}, 50) // 50ms internal debounce to prevent multiple calls
}

private _setOutline(outlineTree: OutlineTree[], editor: TextEditor, isLarge: boolean) {
// skip rendering if it is the same
// TIME 0.2-1.2ms // the check itself takes ~0.2-0.5ms, so it is better than rerendering
if (this.lastEntries !== undefined && hasEqualContent(outlineTree, this.lastEntries)) {
Expand All @@ -70,6 +107,8 @@ export class OutlineView {

this.outlineList = createOutlineList(outlineTree, editor, isLarge, this.pointToElementsMap)
this.outlineContent.appendChild(this.outlineList)

this.isRendering = false
}

clearContent() {
Expand All @@ -80,6 +119,11 @@ export class OutlineView {
this.lastEntries = undefined
}

setStatus(id: "noEditor" | "noProvider") {
this.presentStatus(statuses[id])
this.isRendering = false
}

presentStatus(status: { title: string; description: string }) {
this.clearContent()

Expand Down