From f2574817a9b4c276007b055cd96d6bb36d382676 Mon Sep 17 00:00:00 2001 From: Sebastian Balcerowiak Date: Sun, 8 Feb 2026 00:30:39 +0100 Subject: [PATCH 1/7] Add service API to background-tips and redistribute tips to their packages --- .../lib/background-tips-view.js | 222 +++++++------ .../background-tips/lib/background-tips.js | 39 ++- packages/background-tips/lib/tips.js | 15 +- packages/background-tips/package.json | 8 + .../spec/async-spec-helpers.js | 101 +++--- .../spec/background-tips-spec.js | 296 ++++++++++-------- .../lib/command-palette-package.js | 6 + packages/command-palette/package.json | 7 + packages/find-and-replace/lib/find.js | 6 + packages/find-and-replace/package.json | 5 + packages/fuzzy-finder/lib/main.js | 6 + packages/fuzzy-finder/package.json | 5 + packages/settings-view/lib/main.js | 6 + packages/settings-view/package.json | 5 + packages/symbols-view/lib/main.js | 6 + packages/symbols-view/package.json | 5 + packages/tree-view/lib/tree-view-package.js | 7 + packages/tree-view/package.json | 5 + 18 files changed, 460 insertions(+), 290 deletions(-) diff --git a/packages/background-tips/lib/background-tips-view.js b/packages/background-tips/lib/background-tips-view.js index d6efce494b..0088d97c15 100644 --- a/packages/background-tips/lib/background-tips-view.js +++ b/packages/background-tips/lib/background-tips-view.js @@ -1,156 +1,184 @@ -const _ = require('underscore-plus') -const {CompositeDisposable, Disposable} = require('atom') -const Tips = require('./tips') +const _ = require("underscore-plus"); +const { CompositeDisposable, Disposable } = require("atom"); const TEMPLATE = `\ \ -` - -module.exports = -class BackgroundTipsElement { - constructor () { - this.element = document.createElement('background-tips') - this.index = -1 - this.workspaceCenter = atom.workspace.getCenter() - - this.startDelay = 1000 - this.displayDuration = 10000 - this.fadeDuration = 300 - - this.disposables = new CompositeDisposable() - - const visibilityCallback = () => this.updateVisibility() - - this.disposables.add(this.workspaceCenter.onDidAddPane(visibilityCallback)) - this.disposables.add(this.workspaceCenter.onDidDestroyPane(visibilityCallback)) - this.disposables.add(this.workspaceCenter.onDidChangeActivePaneItem(visibilityCallback)) - - atom.getCurrentWindow().on('blur', visibilityCallback) - atom.getCurrentWindow().on('focus', visibilityCallback) - - this.disposables.add(new Disposable(() => atom.getCurrentWindow().removeListener('blur', visibilityCallback))) - this.disposables.add(new Disposable(() => atom.getCurrentWindow().removeListener('focus', visibilityCallback))) - - this.startTimeout = setTimeout(() => this.start(), this.startDelay) +`; + +module.exports = class BackgroundTipsElement { + constructor(main) { + this.main = main; + this.element = document.createElement("background-tips"); + this.index = -1; + this.workspaceCenter = atom.workspace.getCenter(); + + this.startDelay = 1000; + this.displayDuration = 10000; + this.fadeDuration = 300; + + this.renderedTips = []; + this.tipsRendered = false; + + this.disposables = new CompositeDisposable(); + + const visibilityCallback = () => this.updateVisibility(); + + this.disposables.add(this.workspaceCenter.onDidAddPane(visibilityCallback)); + this.disposables.add( + this.workspaceCenter.onDidDestroyPane(visibilityCallback) + ); + this.disposables.add( + this.workspaceCenter.onDidChangeActivePaneItem(visibilityCallback) + ); + + atom.getCurrentWindow().on("blur", visibilityCallback); + atom.getCurrentWindow().on("focus", visibilityCallback); + + this.disposables.add( + new Disposable(() => + atom.getCurrentWindow().removeListener("blur", visibilityCallback) + ) + ); + this.disposables.add( + new Disposable(() => + atom.getCurrentWindow().removeListener("focus", visibilityCallback) + ) + ); + + this.startTimeout = setTimeout(() => this.start(), this.startDelay); } - destroy () { - this.stop() - this.disposables.dispose() + destroy() { + this.stop(); + this.disposables.dispose(); } - attach () { - this.element.innerHTML = TEMPLATE - this.message = this.element.querySelector('.message') + attach() { + this.element.innerHTML = TEMPLATE; + this.message = this.element.querySelector(".message"); - const paneView = atom.views.getView(this.workspaceCenter.getActivePane()) - const itemViews = paneView.querySelector('.item-views') - let top = 0 + const paneView = atom.views.getView(this.workspaceCenter.getActivePane()); + const itemViews = paneView.querySelector(".item-views"); + let top = 0; if (itemViews && itemViews.offsetTop) { - top = itemViews.offsetTop + top = itemViews.offsetTop; } - this.element.style.top = top + 'px' - paneView.appendChild(this.element) + this.element.style.top = top + "px"; + paneView.appendChild(this.element); } - detach () { - this.element.remove() + detach() { + this.element.remove(); } - updateVisibility () { + updateVisibility() { if (this.shouldBeAttached()) { - this.start() + this.start(); } else { - this.stop() + this.stop(); } } - shouldBeAttached () { - return this.workspaceCenter.getPanes().length === 1 && - this.workspaceCenter.getActivePaneItem() == null && - atom.getCurrentWindow().isFocused() + shouldBeAttached() { + return ( + this.workspaceCenter.getPanes().length === 1 && + this.workspaceCenter.getActivePaneItem() == null && + atom.getCurrentWindow().isFocused() + ); } - start () { - if (!this.shouldBeAttached() || this.interval != null) return - this.renderTips() - this.randomizeIndex() - this.attach() - this.showNextTip() - this.interval = setInterval(() => this.showNextTip(), this.displayDuration) + start() { + if (!this.shouldBeAttached() || this.interval != null) return; + this.renderTips(); + this.randomizeIndex(); + this.attach(); + this.showNextTip(); + this.interval = setInterval(() => this.showNextTip(), this.displayDuration); } - stop () { - this.element.remove() + stop() { + this.element.remove(); if (this.interval != null) { - clearInterval(this.interval) + clearInterval(this.interval); } - clearTimeout(this.startTimeout) - clearTimeout(this.nextTipTimeout) - this.interval = null + clearTimeout(this.startTimeout); + clearTimeout(this.nextTipTimeout); + this.interval = null; } - randomizeIndex () { - const len = Tips.length - this.index = Math.round(Math.random() * len) % len + randomizeIndex() { + const len = this.renderedTips.length; + this.index = len > 0 ? Math.round(Math.random() * len) % len : 0; } - showNextTip () { - this.index = ++this.index % Tips.length - this.message.classList.remove('fade-in') + showNextTip() { + if (this.renderedTips.length === 0) return; + this.index = ++this.index % this.renderedTips.length; + this.message.classList.remove("fade-in"); this.nextTipTimeout = setTimeout(() => { - this.message.innerHTML = Tips[this.index] - this.message.classList.add('fade-in') - }, this.fadeDuration) + this.message.innerHTML = this.renderedTips[this.index]; + this.message.classList.add("fade-in"); + }, this.fadeDuration); } - renderTips () { - if (this.tipsRendered) return - for (let i = 0; i < Tips.length; i++) { - const tip = Tips[i] - Tips[i] = this.renderTip(tip) + renderTips() { + if (this.tipsRendered) return; + const tips = this.main.getTips(); + this.renderedTips = tips.map((tip) => this.renderTip(tip)); + this.tipsRendered = true; + } + + tipsChanged() { + this.tipsRendered = false; + if (this.interval != null) { + this.renderTips(); + this.index = Math.min(this.index, this.renderedTips.length - 1); } - this.tipsRendered = true } - renderTip (str) { + renderTip(str) { str = str.replace(/\{(.+)\}/g, (match, command) => { - let binding, scope - const scopeAndCommand = command.split('>') + let binding, scope; + const scopeAndCommand = command.split(">"); if (scopeAndCommand.length > 1) { - [scope, command] = scopeAndCommand + [scope, command] = scopeAndCommand; } - const bindings = atom.keymaps.findKeyBindings({command: command.trim()}) + const bindings = atom.keymaps.findKeyBindings({ + command: command.trim(), + }); if (scope) { for (binding of bindings) { - if (binding.selector === scope) break + if (binding.selector === scope) break; } } else { - binding = this.getKeyBindingForCurrentPlatform(bindings) + binding = this.getKeyBindingForCurrentPlatform(bindings); } if (binding && binding.keystrokes) { - const keystrokeLabel = _.humanizeKeystroke(binding.keystrokes).replace(/\s+/g, ' ') - return `${keystrokeLabel}` + const keystrokeLabel = _.humanizeKeystroke(binding.keystrokes).replace( + /\s+/g, + " " + ); + return `${keystrokeLabel}`; } else { - return command + return command; } - }) - return str + }); + return str; } - getKeyBindingForCurrentPlatform (bindings) { - if (!bindings || !bindings.length) return + getKeyBindingForCurrentPlatform(bindings) { + if (!bindings || !bindings.length) return; for (let binding of bindings) { if (binding.selector.indexOf(process.platform) !== -1) { - return binding + return binding; } } - return bindings[0] + return bindings[0]; } -} +}; diff --git a/packages/background-tips/lib/background-tips.js b/packages/background-tips/lib/background-tips.js index 3c827be7e2..9bfea98097 100644 --- a/packages/background-tips/lib/background-tips.js +++ b/packages/background-tips/lib/background-tips.js @@ -1,11 +1,36 @@ -const BackgroundTipsView = require('./background-tips-view') +const { Disposable } = require("atom"); +const BackgroundTipsView = require("./background-tips-view"); module.exports = { - activate () { - this.backgroundTipsView = new BackgroundTipsView() + activate() { + this.defaultTips = require("./tips"); + this.addedTips = new Set(); + this.backgroundTipsView = new BackgroundTipsView(this); }, - deactivate () { - this.backgroundTipsView.destroy() - } -} + deactivate() { + this.backgroundTipsView.destroy(); + this.addedTips.clear(); + }, + + getTips() { + let all = [...this.defaultTips]; + for (const tips of this.addedTips) { + all = all.concat(tips); + } + return all; + }, + + provideBackgroundTips() { + return { + addTips: (tips) => { + this.addedTips.add(tips); + this.backgroundTipsView.tipsChanged(); + return new Disposable(() => { + this.addedTips.delete(tips); + this.backgroundTipsView.tipsChanged(); + }); + }, + }; + }, +}; diff --git a/packages/background-tips/lib/tips.js b/packages/background-tips/lib/tips.js index fa70dc48a7..ad22dedfd9 100644 --- a/packages/background-tips/lib/tips.js +++ b/packages/background-tips/lib/tips.js @@ -1,14 +1,3 @@ module.exports = [ - 'Close panels like find and replace with {body>core:cancel}', - `Everything ${atom.branding.name} can do is in the Command Palette. See it by using {command-palette:toggle}`, - 'You can quickly open files with the Fuzzy Finder. Try it by using {fuzzy-finder:toggle-file-finder}', - 'You can toggle the Tree View with {tree-view:toggle}', - 'You can focus the Tree View with {tree-view:toggle-focus}', - 'You can toggle the Git tab with {github:toggle-git-tab}', - 'You can focus the Git tab with {github:toggle-git-tab-focus}', - 'You can toggle the GitHub tab with {github:toggle-github-tab}', - 'You can focus the GitHub tab with {github:toggle-github-tab-focus}', - 'You can split a pane with {pane:split-right-and-copy-active-item}', - 'You can jump to a method in the editor using {symbols-view:toggle-file-symbols}', - 'You can install packages and themes from the Settings View {settings-view:open}' -] + "Split your editor into multiple panes with {pane:split-right-and-copy-active-item}", +]; diff --git a/packages/background-tips/package.json b/packages/background-tips/package.json index d3b7bb1145..736d581c0f 100644 --- a/packages/background-tips/package.json +++ b/packages/background-tips/package.json @@ -9,6 +9,14 @@ "engines": { "atom": ">0.42.0" }, + "providedServices": { + "background-tips": { + "description": "Allows packages to register tips shown when no editors are open", + "versions": { + "1.0.0": "provideBackgroundTips" + } + } + }, "dependencies": { "underscore-plus": "1.x" } diff --git a/packages/background-tips/spec/async-spec-helpers.js b/packages/background-tips/spec/async-spec-helpers.js index 73002c049a..c989e292c8 100644 --- a/packages/background-tips/spec/async-spec-helpers.js +++ b/packages/background-tips/spec/async-spec-helpers.js @@ -1,103 +1,106 @@ /** @babel */ -export function beforeEach (fn) { +export function beforeEach(fn) { global.beforeEach(function () { - const result = fn() + const result = fn(); if (result instanceof Promise) { - waitsForPromise(() => result) + waitsForPromise(() => result); } - }) + }); } -export function afterEach (fn) { +export function afterEach(fn) { global.afterEach(function () { - const result = fn() + const result = fn(); if (result instanceof Promise) { - waitsForPromise(() => result) + waitsForPromise(() => result); } - }) + }); } -['it', 'fit', 'ffit', 'fffit'].forEach(function (name) { +["it", "fit", "ffit", "fffit"].forEach(function (name) { module.exports[name] = function (description, fn) { if (fn === undefined) { - global[name](description) - return + global[name](description); + return; } global[name](description, function () { - const result = fn() + const result = fn(); if (result instanceof Promise) { - waitsForPromise(() => result) + waitsForPromise(() => result); } - }) - } -}) + }); + }; +}); -export async function conditionPromise (condition, description = 'anonymous condition') { - const startTime = Date.now() +export async function conditionPromise( + condition, + description = "anonymous condition" +) { + const startTime = Date.now(); while (true) { - await timeoutPromise(100) + await timeoutPromise(100); if (await condition()) { - return + return; } if (Date.now() - startTime > 5000) { - throw new Error('Timed out waiting on ' + description) + throw new Error("Timed out waiting on " + description); } } } -export function timeoutPromise (timeout) { +export function timeoutPromise(timeout) { return new Promise(function (resolve) { - global.setTimeout(resolve, timeout) - }) + global.setTimeout(resolve, timeout); + }); } -function waitsForPromise (fn) { - const promise = fn() - global.waitsFor('spec promise to resolve', function (done) { +function waitsForPromise(fn) { + const promise = fn(); + global.waitsFor("spec promise to resolve", function (done) { promise.then(done, function (error) { - jasmine.getEnv().currentSpec.fail(error) - done() - }) - }) + jasmine.getEnv().currentSpec.fail(error); + done(); + }); + }); } -export function emitterEventPromise (emitter, event, timeout = 15000) { +export function emitterEventPromise(emitter, event, timeout = 15000) { return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { - reject(new Error(`Timed out waiting for '${event}' event`)) - }, timeout) + reject(new Error(`Timed out waiting for '${event}' event`)); + }, timeout); emitter.once(event, () => { - clearTimeout(timeoutHandle) - resolve() - }) - }) + clearTimeout(timeoutHandle); + resolve(); + }); + }); } -export function promisify (original) { +export function promisify(original) { return function (...args) { return new Promise((resolve, reject) => { args.push((err, ...results) => { if (err) { - reject(err) + reject(err); } else { - resolve(...results) + resolve(...results); } - }) + }); - return original(...args) - }) - } + return original(...args); + }); + }; } -export function promisifySome (obj, fnNames) { - const result = {} +export function promisifySome(obj, fnNames) { + const result = {}; for (const fnName of fnNames) { - result[fnName] = promisify(obj[fnName]) + result[fnName] = promisify(obj[fnName]); } - return result + return result; } diff --git a/packages/background-tips/spec/background-tips-spec.js b/packages/background-tips/spec/background-tips-spec.js index 86a3c78bfb..7f91b8734a 100644 --- a/packages/background-tips/spec/background-tips-spec.js +++ b/packages/background-tips/spec/background-tips-spec.js @@ -1,142 +1,190 @@ -const {it, fit, ffit, afterEach, beforeEach, emitterEventPromise} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars - -describe('BackgroundTips', () => { - let workspaceElement +const { + it, + fit, + ffit, + afterEach, + beforeEach, + emitterEventPromise, +} = require("./async-spec-helpers"); + +describe("BackgroundTips", () => { + let workspaceElement; const activatePackage = async () => { - const {mainModule} = await atom.packages.activatePackage('background-tips') - return mainModule.backgroundTipsView - } + const { mainModule } = await atom.packages.activatePackage( + "background-tips" + ); + return mainModule.backgroundTipsView; + }; beforeEach(() => { - workspaceElement = atom.views.getView(atom.workspace) - jasmine.attachToDOM(workspaceElement) - jasmine.useMockClock() - spyOn(atom.getCurrentWindow(), 'isFocused').andReturn(true) - }) + workspaceElement = atom.views.getView(atom.workspace); + jasmine.attachToDOM(workspaceElement); + jasmine.useMockClock(); + spyOn(atom.getCurrentWindow(), "isFocused").andReturn(true); + }); - describe('when the package is activated when there is only one pane', () => { + describe("when the package is activated when there is only one pane", () => { beforeEach(() => { - expect(atom.workspace.getCenter().getPanes().length).toBe(1) - }) - - describe('when the pane is empty', () => { - it('attaches the view after a delay', async () => { - expect(atom.workspace.getActivePane().getItems().length).toBe(0) - - const backgroundTipsView = await activatePackage() - expect(backgroundTipsView.element.parentNode).toBeFalsy() - advanceClock(backgroundTipsView.startDelay + 1) - expect(backgroundTipsView.element.parentNode).toBeTruthy() - }) - }) - - describe('when the pane is not empty', () => { - it('does not attach the view', async () => { - await atom.workspace.open() - - const backgroundTipsView = await activatePackage() - advanceClock(backgroundTipsView.startDelay + 1) - expect(backgroundTipsView.element.parentNode).toBeFalsy() - }) - }) - - describe('when a second pane is created', () => { - it('detaches the view', async () => { - const backgroundTipsView = await activatePackage() - advanceClock(backgroundTipsView.startDelay + 1) - expect(backgroundTipsView.element.parentNode).toBeTruthy() - - atom.workspace.getActivePane().splitRight() - expect(backgroundTipsView.element.parentNode).toBeFalsy() - }) - }) - }) - - describe('when the package is activated when there are multiple panes', () => { + expect(atom.workspace.getCenter().getPanes().length).toBe(1); + }); + + describe("when the pane is empty", () => { + it("attaches the view after a delay", async () => { + expect(atom.workspace.getActivePane().getItems().length).toBe(0); + + const backgroundTipsView = await activatePackage(); + expect(backgroundTipsView.element.parentNode).toBeFalsy(); + advanceClock(backgroundTipsView.startDelay + 1); + expect(backgroundTipsView.element.parentNode).toBeTruthy(); + }); + }); + + describe("when the pane is not empty", () => { + it("does not attach the view", async () => { + await atom.workspace.open(); + + const backgroundTipsView = await activatePackage(); + advanceClock(backgroundTipsView.startDelay + 1); + expect(backgroundTipsView.element.parentNode).toBeFalsy(); + }); + }); + + describe("when a second pane is created", () => { + it("detaches the view", async () => { + const backgroundTipsView = await activatePackage(); + advanceClock(backgroundTipsView.startDelay + 1); + expect(backgroundTipsView.element.parentNode).toBeTruthy(); + + atom.workspace.getActivePane().splitRight(); + expect(backgroundTipsView.element.parentNode).toBeFalsy(); + }); + }); + }); + + describe("when the package is activated when there are multiple panes", () => { beforeEach(() => { - atom.workspace.getActivePane().splitRight() - expect(atom.workspace.getCenter().getPanes().length).toBe(2) - }) - - it('does not attach the view', async () => { - const backgroundTipsView = await activatePackage() - advanceClock(backgroundTipsView.startDelay + 1) - expect(backgroundTipsView.element.parentNode).toBeFalsy() - }) - - describe('when all but the last pane is destroyed', () => { - it('attaches the view', async () => { - const backgroundTipsView = await activatePackage() - atom.workspace.getActivePane().destroy() - advanceClock(backgroundTipsView.startDelay + 1) - expect(backgroundTipsView.element.parentNode).toBeTruthy() - - atom.workspace.getActivePane().splitRight() - expect(backgroundTipsView.element.parentNode).toBeFalsy() - - atom.workspace.getActivePane().destroy() - expect(backgroundTipsView.element.parentNode).toBeTruthy() - }) - }) - }) - - describe('when the view is attached', () => { - let backgroundTipsView + atom.workspace.getActivePane().splitRight(); + expect(atom.workspace.getCenter().getPanes().length).toBe(2); + }); + + it("does not attach the view", async () => { + const backgroundTipsView = await activatePackage(); + advanceClock(backgroundTipsView.startDelay + 1); + expect(backgroundTipsView.element.parentNode).toBeFalsy(); + }); + + describe("when all but the last pane is destroyed", () => { + it("attaches the view", async () => { + const backgroundTipsView = await activatePackage(); + atom.workspace.getActivePane().destroy(); + advanceClock(backgroundTipsView.startDelay + 1); + expect(backgroundTipsView.element.parentNode).toBeTruthy(); + + atom.workspace.getActivePane().splitRight(); + expect(backgroundTipsView.element.parentNode).toBeFalsy(); + + atom.workspace.getActivePane().destroy(); + expect(backgroundTipsView.element.parentNode).toBeTruthy(); + }); + }); + }); + + describe("when the view is attached", () => { + let backgroundTipsView; beforeEach(async () => { - expect(atom.workspace.getCenter().getPanes().length).toBe(1) - - backgroundTipsView = await activatePackage() - advanceClock(backgroundTipsView.startDelay) - advanceClock(backgroundTipsView.fadeDuration) - }) - - it('has text in the message', () => { - expect(backgroundTipsView.element.parentNode).toBeTruthy() - expect(backgroundTipsView.message.textContent).toBeTruthy() - }) - - it('changes text in the message', async () => { - const oldText = backgroundTipsView.message.textContent - advanceClock(backgroundTipsView.displayDuration) - advanceClock(backgroundTipsView.fadeDuration) - expect(backgroundTipsView.message.textContent).not.toEqual(oldText) - }) - }) - - describe('when Atom is not focused but all other requirements are satisfied', () => { + expect(atom.workspace.getCenter().getPanes().length).toBe(1); + + backgroundTipsView = await activatePackage(); + advanceClock(backgroundTipsView.startDelay); + advanceClock(backgroundTipsView.fadeDuration); + }); + + it("has text in the message", () => { + expect(backgroundTipsView.element.parentNode).toBeTruthy(); + expect(backgroundTipsView.message.textContent).toBeTruthy(); + }); + + it("changes text in the message", async () => { + const oldText = backgroundTipsView.message.textContent; + advanceClock(backgroundTipsView.displayDuration); + advanceClock(backgroundTipsView.fadeDuration); + expect(backgroundTipsView.message.textContent).not.toEqual(oldText); + }); + }); + + describe("provideBackgroundTips service", () => { + it("allows adding and removing tips", async () => { + const { mainModule } = await atom.packages.activatePackage( + "background-tips" + ); + const service = mainModule.provideBackgroundTips(); + + expect(typeof service.addTips).toBe("function"); + + const originalCount = mainModule.getTips().length; + const disposable = service.addTips(["Custom tip 1", "Custom tip 2"]); + expect(mainModule.getTips().length).toBe(originalCount + 2); + expect(mainModule.getTips()).toContain("Custom tip 1"); + expect(mainModule.getTips()).toContain("Custom tip 2"); + + disposable.dispose(); + expect(mainModule.getTips().length).toBe(originalCount); + expect(mainModule.getTips()).not.toContain("Custom tip 1"); + }); + + it("shows contributed tips when cycling", async () => { + const backgroundTipsView = await activatePackage(); + const { mainModule } = atom.packages.getActivePackage("background-tips"); + + const service = mainModule.provideBackgroundTips(); + service.addTips(["Unique test tip for spec"]); + + advanceClock(backgroundTipsView.startDelay); + advanceClock(backgroundTipsView.fadeDuration); + + // The contributed tip should be in the rendered tips + expect( + backgroundTipsView.renderedTips.some((t) => + t.includes("Unique test tip for spec") + ) + ).toBe(true); + }); + }); + + describe("when Atom is not focused but all other requirements are satisfied", () => { beforeEach(() => { - jasmine.unspy(atom.getCurrentWindow(), 'isFocused') - spyOn(atom.getCurrentWindow(), 'isFocused').andReturn(false) - }) + jasmine.unspy(atom.getCurrentWindow(), "isFocused"); + spyOn(atom.getCurrentWindow(), "isFocused").andReturn(false); + }); - it('does not display the background tips', async () => { - expect(atom.workspace.getActivePane().getItems().length).toBe(0) + it("does not display the background tips", async () => { + expect(atom.workspace.getActivePane().getItems().length).toBe(0); - const backgroundTipsView = await activatePackage() - expect(backgroundTipsView.element.parentNode).toBeFalsy() - advanceClock(backgroundTipsView.startDelay + 1) - expect(backgroundTipsView.element.parentNode).toBeFalsy() - }) + const backgroundTipsView = await activatePackage(); + expect(backgroundTipsView.element.parentNode).toBeFalsy(); + advanceClock(backgroundTipsView.startDelay + 1); + expect(backgroundTipsView.element.parentNode).toBeFalsy(); + }); - it('reactivates the background tips if the focus event is received', async () => { - expect(atom.workspace.getActivePane().getItems().length).toBe(0) + it("reactivates the background tips if the focus event is received", async () => { + expect(atom.workspace.getActivePane().getItems().length).toBe(0); - const backgroundTipsView = await activatePackage() - advanceClock(backgroundTipsView.startDelay + 1) - expect(backgroundTipsView.element.parentNode).toBeFalsy() + const backgroundTipsView = await activatePackage(); + advanceClock(backgroundTipsView.startDelay + 1); + expect(backgroundTipsView.element.parentNode).toBeFalsy(); - jasmine.unspy(atom.getCurrentWindow(), 'isFocused') - spyOn(atom.getCurrentWindow(), 'isFocused').andReturn(true) + jasmine.unspy(atom.getCurrentWindow(), "isFocused"); + spyOn(atom.getCurrentWindow(), "isFocused").andReturn(true); - const focusEvent = emitterEventPromise(atom.getCurrentWindow(), 'focus') - atom.getCurrentWindow().emit('focus') // Manually emit to prevent actually blurring + refocusing the window + const focusEvent = emitterEventPromise(atom.getCurrentWindow(), "focus"); + atom.getCurrentWindow().emit("focus"); // Manually emit to prevent actually blurring + refocusing the window - await focusEvent + await focusEvent; - advanceClock(backgroundTipsView.startDelay + 1) - expect(backgroundTipsView.element.parentNode).toBeTruthy() - }) - }) -}) + advanceClock(backgroundTipsView.startDelay + 1); + expect(backgroundTipsView.element.parentNode).toBeTruthy(); + }); + }); +}); diff --git a/packages/command-palette/lib/command-palette-package.js b/packages/command-palette/lib/command-palette-package.js index 38b8ba7e28..53abccfef5 100644 --- a/packages/command-palette/lib/command-palette-package.js +++ b/packages/command-palette/lib/command-palette-package.js @@ -21,6 +21,12 @@ class CommandPalettePackage { return this.commandPaletteView.show() } + consumeBackgroundTips (service) { + return service.addTips([ + `The Command Palette lets you access all of ${atom.branding.name}'s commands. Open it with {command-palette:toggle}` + ]) + } + async deactivate () { this.disposables.dispose() await this.commandPaletteView.destroy() diff --git a/packages/command-palette/package.json b/packages/command-palette/package.json index f791b96b8e..aa6be64da2 100644 --- a/packages/command-palette/package.json +++ b/packages/command-palette/package.json @@ -24,6 +24,13 @@ "semver": "^5.4.1", "sinon": "^3.2.1" }, + "consumedServices": { + "background-tips": { + "versions": { + "1.0.0": "consumeBackgroundTips" + } + } + }, "configSchema": { "preserveLastSearch": { "type": "boolean", diff --git a/packages/find-and-replace/lib/find.js b/packages/find-and-replace/lib/find.js index cbfeb843d2..4986f90605 100644 --- a/packages/find-and-replace/lib/find.js +++ b/packages/find-and-replace/lib/find.js @@ -202,6 +202,12 @@ module.exports = { } }, + consumeBackgroundTips(service) { + return service.addTips([ + 'Dismiss panels like Find and Replace with {body>core:cancel}' + ]); + }, + consumeAutocompleteWatchEditor(watchEditor) { this.autocompleteWatchEditor = watchEditor; atom.config.observe( diff --git a/packages/find-and-replace/package.json b/packages/find-and-replace/package.json index 1c06bab840..7eb4a91eea 100644 --- a/packages/find-and-replace/package.json +++ b/packages/find-and-replace/package.json @@ -55,6 +55,11 @@ "versions": { "1.0.0": "consumeElementIcons" } + }, + "background-tips": { + "versions": { + "1.0.0": "consumeBackgroundTips" + } } }, "providedServices": { diff --git a/packages/fuzzy-finder/lib/main.js b/packages/fuzzy-finder/lib/main.js index 1a5aa055d1..7e9bf1349d 100644 --- a/packages/fuzzy-finder/lib/main.js +++ b/packages/fuzzy-finder/lib/main.js @@ -77,6 +77,12 @@ module.exports = { return new Disposable(() => metricsReporter.unsetReporter()) }, + consumeBackgroundTips (service) { + return service.addTips([ + 'Quickly open any file in your project with the Fuzzy Finder using {fuzzy-finder:toggle-file-finder}' + ]) + }, + serialize () { const paths = {} for (let editor of atom.workspace.getTextEditors()) { diff --git a/packages/fuzzy-finder/package.json b/packages/fuzzy-finder/package.json index f255a35d15..1fb4708352 100644 --- a/packages/fuzzy-finder/package.json +++ b/packages/fuzzy-finder/package.json @@ -41,6 +41,11 @@ "versions": { "^1.1.0": "consumeMetricsReporter" } + }, + "background-tips": { + "versions": { + "1.0.0": "consumeBackgroundTips" + } } }, "configSchema": { diff --git a/packages/settings-view/lib/main.js b/packages/settings-view/lib/main.js index 98781d3524..6c39016593 100644 --- a/packages/settings-view/lib/main.js +++ b/packages/settings-view/lib/main.js @@ -84,6 +84,12 @@ module.exports = { } }, + consumeBackgroundTips (service) { + return service.addTips([ + 'Install packages, themes, and customize your editor in Settings {settings-view:open}' + ]) + }, + consumeSnippets (snippets) { if (typeof snippets.getUnparsedSnippets === 'function') { SnippetsProvider.getSnippets = snippets.getUnparsedSnippets.bind(snippets) diff --git a/packages/settings-view/package.json b/packages/settings-view/package.json index 6826a02fc3..9db5539c50 100644 --- a/packages/settings-view/package.json +++ b/packages/settings-view/package.json @@ -65,6 +65,11 @@ "versions": { "0.1.0": "consumeSnippets" } + }, + "background-tips": { + "versions": { + "1.0.0": "consumeBackgroundTips" + } } }, "deserializers": { diff --git a/packages/symbols-view/lib/main.js b/packages/symbols-view/lib/main.js index a8aedeed78..31b8cf9368 100644 --- a/packages/symbols-view/lib/main.js +++ b/packages/symbols-view/lib/main.js @@ -115,6 +115,12 @@ module.exports = { this.subscriptions = null; }, + consumeBackgroundTips (service) { + return service.addTips([ + 'Navigate to any function or symbol in the current file with {symbols-view:toggle-file-symbols}' + ]) + }, + consumeSymbolProvider(provider) { if (Array.isArray(provider)) { this.broker.add(...provider); diff --git a/packages/symbols-view/package.json b/packages/symbols-view/package.json index 8e10033008..ef4526d2fa 100644 --- a/packages/symbols-view/package.json +++ b/packages/symbols-view/package.json @@ -68,6 +68,11 @@ "versions": { "1.0.0": "consumeSymbolProvider" } + }, + "background-tips": { + "versions": { + "1.0.0": "consumeBackgroundTips" + } } }, "providedServices": { diff --git a/packages/tree-view/lib/tree-view-package.js b/packages/tree-view/lib/tree-view-package.js index 6709639a66..25b55fa271 100644 --- a/packages/tree-view/lib/tree-view-package.js +++ b/packages/tree-view/lib/tree-view-package.js @@ -58,6 +58,13 @@ class TreeViewPackage { }) } + consumeBackgroundTips (service) { + return service.addTips([ + 'Show or hide the Tree View with {tree-view:toggle}', + 'Jump to the Tree View without leaving your keyboard using {tree-view:toggle-focus}' + ]) + } + provideTreeView () { return { selectedPaths: () => this.getTreeViewInstance().selectedPaths(), diff --git a/packages/tree-view/package.json b/packages/tree-view/package.json index faef314eff..559c29dd18 100644 --- a/packages/tree-view/package.json +++ b/packages/tree-view/package.json @@ -29,6 +29,11 @@ "versions": { "1.0.0": "consumeElementIcons" } + }, + "background-tips": { + "versions": { + "1.0.0": "consumeBackgroundTips" + } } }, "providedServices": { From e07b7e6f4fd4f39496403a32aaf4e76997b80b82 Mon Sep 17 00:00:00 2001 From: Sebastian Balcerowiak Date: Sun, 8 Feb 2026 01:10:38 +0100 Subject: [PATCH 2/7] Add more default tips to fix "changes text" test with single-tip array --- packages/background-tips/lib/tips.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/background-tips/lib/tips.js b/packages/background-tips/lib/tips.js index ad22dedfd9..8e7c5e1575 100644 --- a/packages/background-tips/lib/tips.js +++ b/packages/background-tips/lib/tips.js @@ -1,3 +1,5 @@ module.exports = [ "Split your editor into multiple panes with {pane:split-right-and-copy-active-item}", + "You can move lines of text up and down with {editor:move-line-up} and {editor:move-line-down}", + "Toggle line comments quickly with {editor:toggle-line-comments}", ]; From eeeeb4a37b84cd05202b501bdb4737f53fc7dac0 Mon Sep 17 00:00:00 2001 From: Sebastian Balcerowiak Date: Tue, 3 Mar 2026 12:21:16 +0100 Subject: [PATCH 3/7] Rename addTips to registerTips and fix service lifecycle for tip disposal --- packages/background-tips/lib/background-tips.js | 2 +- packages/background-tips/spec/background-tips-spec.js | 6 +++--- packages/command-palette/lib/command-palette-package.js | 4 +++- packages/find-and-replace/lib/find.js | 4 +++- packages/fuzzy-finder/lib/main.js | 4 +++- packages/settings-view/lib/main.js | 5 ++++- packages/symbols-view/lib/main.js | 7 ++++++- packages/tree-view/lib/tree-view-package.js | 4 +++- 8 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/background-tips/lib/background-tips.js b/packages/background-tips/lib/background-tips.js index 9bfea98097..8e7f0d325c 100644 --- a/packages/background-tips/lib/background-tips.js +++ b/packages/background-tips/lib/background-tips.js @@ -23,7 +23,7 @@ module.exports = { provideBackgroundTips() { return { - addTips: (tips) => { + registerTips: (tips) => { this.addedTips.add(tips); this.backgroundTipsView.tipsChanged(); return new Disposable(() => { diff --git a/packages/background-tips/spec/background-tips-spec.js b/packages/background-tips/spec/background-tips-spec.js index 7f91b8734a..a967b6a0d3 100644 --- a/packages/background-tips/spec/background-tips-spec.js +++ b/packages/background-tips/spec/background-tips-spec.js @@ -121,10 +121,10 @@ describe("BackgroundTips", () => { ); const service = mainModule.provideBackgroundTips(); - expect(typeof service.addTips).toBe("function"); + expect(typeof service.registerTips).toBe("function"); const originalCount = mainModule.getTips().length; - const disposable = service.addTips(["Custom tip 1", "Custom tip 2"]); + const disposable = service.registerTips(["Custom tip 1", "Custom tip 2"]); expect(mainModule.getTips().length).toBe(originalCount + 2); expect(mainModule.getTips()).toContain("Custom tip 1"); expect(mainModule.getTips()).toContain("Custom tip 2"); @@ -139,7 +139,7 @@ describe("BackgroundTips", () => { const { mainModule } = atom.packages.getActivePackage("background-tips"); const service = mainModule.provideBackgroundTips(); - service.addTips(["Unique test tip for spec"]); + service.registerTips(["Unique test tip for spec"]); advanceClock(backgroundTipsView.startDelay); advanceClock(backgroundTipsView.fadeDuration); diff --git a/packages/command-palette/lib/command-palette-package.js b/packages/command-palette/lib/command-palette-package.js index 53abccfef5..a3154ab570 100644 --- a/packages/command-palette/lib/command-palette-package.js +++ b/packages/command-palette/lib/command-palette-package.js @@ -22,9 +22,11 @@ class CommandPalettePackage { } consumeBackgroundTips (service) { - return service.addTips([ + const disposable = service.registerTips([ `The Command Palette lets you access all of ${atom.branding.name}'s commands. Open it with {command-palette:toggle}` ]) + this.disposables.add(disposable) + return disposable } async deactivate () { diff --git a/packages/find-and-replace/lib/find.js b/packages/find-and-replace/lib/find.js index 4986f90605..b118441c63 100644 --- a/packages/find-and-replace/lib/find.js +++ b/packages/find-and-replace/lib/find.js @@ -203,9 +203,11 @@ module.exports = { }, consumeBackgroundTips(service) { - return service.addTips([ + const disposable = service.registerTips([ 'Dismiss panels like Find and Replace with {body>core:cancel}' ]); + this.subscriptions.add(disposable); + return disposable; }, consumeAutocompleteWatchEditor(watchEditor) { diff --git a/packages/fuzzy-finder/lib/main.js b/packages/fuzzy-finder/lib/main.js index 7e9bf1349d..2bb1f7ba06 100644 --- a/packages/fuzzy-finder/lib/main.js +++ b/packages/fuzzy-finder/lib/main.js @@ -78,9 +78,11 @@ module.exports = { }, consumeBackgroundTips (service) { - return service.addTips([ + const disposable = service.registerTips([ 'Quickly open any file in your project with the Fuzzy Finder using {fuzzy-finder:toggle-file-finder}' ]) + this.disposables.add(disposable) + return disposable }, serialize () { diff --git a/packages/settings-view/lib/main.js b/packages/settings-view/lib/main.js index 6c39016593..64438fd99b 100644 --- a/packages/settings-view/lib/main.js +++ b/packages/settings-view/lib/main.js @@ -58,6 +58,7 @@ module.exports = { }, deactivate () { + this.backgroundTipsDisposable?.dispose() if (settingsView) settingsView.destroy() if (statusView) statusView.destroy() settingsView = null @@ -85,9 +86,11 @@ module.exports = { }, consumeBackgroundTips (service) { - return service.addTips([ + const disposable = service.registerTips([ 'Install packages, themes, and customize your editor in Settings {settings-view:open}' ]) + this.backgroundTipsDisposable = disposable + return disposable }, consumeSnippets (snippets) { diff --git a/packages/symbols-view/lib/main.js b/packages/symbols-view/lib/main.js index 31b8cf9368..a61b3a2f89 100644 --- a/packages/symbols-view/lib/main.js +++ b/packages/symbols-view/lib/main.js @@ -111,14 +111,19 @@ module.exports = { this.broker?.destroy(); this.broker = null; + this.backgroundTipsDisposable?.dispose(); + this.backgroundTipsDisposable = null; + this.subscriptions?.dispose(); this.subscriptions = null; }, consumeBackgroundTips (service) { - return service.addTips([ + const disposable = service.registerTips([ 'Navigate to any function or symbol in the current file with {symbols-view:toggle-file-symbols}' ]) + this.backgroundTipsDisposable = disposable + return disposable }, consumeSymbolProvider(provider) { diff --git a/packages/tree-view/lib/tree-view-package.js b/packages/tree-view/lib/tree-view-package.js index 25b55fa271..af0955e952 100644 --- a/packages/tree-view/lib/tree-view-package.js +++ b/packages/tree-view/lib/tree-view-package.js @@ -59,10 +59,12 @@ class TreeViewPackage { } consumeBackgroundTips (service) { - return service.addTips([ + const disposable = service.registerTips([ 'Show or hide the Tree View with {tree-view:toggle}', 'Jump to the Tree View without leaving your keyboard using {tree-view:toggle-focus}' ]) + this.disposables.add(disposable) + return disposable } provideTreeView () { From 9c38f4677dc3547500fbdb460c46deb977e8e147 Mon Sep 17 00:00:00 2001 From: Sebastian Balcerowiak Date: Sat, 14 Mar 2026 20:42:36 +0100 Subject: [PATCH 4/7] Restore deferred package tips to background-tips defaults --- packages/background-tips/lib/tips.js | 3 +++ packages/command-palette/lib/command-palette-package.js | 8 -------- packages/command-palette/package.json | 7 ------- packages/find-and-replace/lib/find.js | 8 -------- packages/find-and-replace/package.json | 5 ----- 5 files changed, 3 insertions(+), 28 deletions(-) diff --git a/packages/background-tips/lib/tips.js b/packages/background-tips/lib/tips.js index 8e7c5e1575..67d4619abf 100644 --- a/packages/background-tips/lib/tips.js +++ b/packages/background-tips/lib/tips.js @@ -2,4 +2,7 @@ module.exports = [ "Split your editor into multiple panes with {pane:split-right-and-copy-active-item}", "You can move lines of text up and down with {editor:move-line-up} and {editor:move-line-down}", "Toggle line comments quickly with {editor:toggle-line-comments}", + // Tips for deferred packages that won't register via services until activated + `The Command Palette lets you access all of ${atom.branding.name}'s commands. Open it with {command-palette:toggle}`, + 'Dismiss panels like Find and Replace with {body>core:cancel}', ]; diff --git a/packages/command-palette/lib/command-palette-package.js b/packages/command-palette/lib/command-palette-package.js index a3154ab570..38b8ba7e28 100644 --- a/packages/command-palette/lib/command-palette-package.js +++ b/packages/command-palette/lib/command-palette-package.js @@ -21,14 +21,6 @@ class CommandPalettePackage { return this.commandPaletteView.show() } - consumeBackgroundTips (service) { - const disposable = service.registerTips([ - `The Command Palette lets you access all of ${atom.branding.name}'s commands. Open it with {command-palette:toggle}` - ]) - this.disposables.add(disposable) - return disposable - } - async deactivate () { this.disposables.dispose() await this.commandPaletteView.destroy() diff --git a/packages/command-palette/package.json b/packages/command-palette/package.json index aa6be64da2..f791b96b8e 100644 --- a/packages/command-palette/package.json +++ b/packages/command-palette/package.json @@ -24,13 +24,6 @@ "semver": "^5.4.1", "sinon": "^3.2.1" }, - "consumedServices": { - "background-tips": { - "versions": { - "1.0.0": "consumeBackgroundTips" - } - } - }, "configSchema": { "preserveLastSearch": { "type": "boolean", diff --git a/packages/find-and-replace/lib/find.js b/packages/find-and-replace/lib/find.js index b118441c63..cbfeb843d2 100644 --- a/packages/find-and-replace/lib/find.js +++ b/packages/find-and-replace/lib/find.js @@ -202,14 +202,6 @@ module.exports = { } }, - consumeBackgroundTips(service) { - const disposable = service.registerTips([ - 'Dismiss panels like Find and Replace with {body>core:cancel}' - ]); - this.subscriptions.add(disposable); - return disposable; - }, - consumeAutocompleteWatchEditor(watchEditor) { this.autocompleteWatchEditor = watchEditor; atom.config.observe( diff --git a/packages/find-and-replace/package.json b/packages/find-and-replace/package.json index 7eb4a91eea..1c06bab840 100644 --- a/packages/find-and-replace/package.json +++ b/packages/find-and-replace/package.json @@ -55,11 +55,6 @@ "versions": { "1.0.0": "consumeElementIcons" } - }, - "background-tips": { - "versions": { - "1.0.0": "consumeBackgroundTips" - } } }, "providedServices": { From 34b8a9a2121a48ba4415f698859aa165ec74a0d3 Mon Sep 17 00:00:00 2001 From: Sebastian Balcerowiak Date: Wed, 6 May 2026 19:40:44 +0200 Subject: [PATCH 5/7] Distribute tips via package.json backgroundTips field --- packages/background-tips/README.md | 18 ++ .../lib/background-tips-view.js | 157 +++++++++--------- .../background-tips/lib/background-tips.js | 41 ++--- packages/background-tips/lib/tips.js | 8 - packages/background-tips/package.json | 25 ++- .../spec/background-tips-spec.js | 75 --------- .../styles/background-tips.less | 4 - packages/command-palette/package.json | 3 + packages/find-and-replace/package.json | 3 + packages/fuzzy-finder/lib/main.js | 8 - packages/fuzzy-finder/package.json | 8 +- packages/settings-view/lib/main.js | 9 - packages/settings-view/package.json | 8 +- packages/symbols-view/lib/main.js | 11 -- packages/symbols-view/package.json | 8 +- packages/tree-view/lib/tree-view-package.js | 9 - packages/tree-view/package.json | 9 +- 17 files changed, 151 insertions(+), 253 deletions(-) delete mode 100644 packages/background-tips/lib/tips.js diff --git a/packages/background-tips/README.md b/packages/background-tips/README.md index 5a3872aa7c..0e117f4e10 100644 --- a/packages/background-tips/README.md +++ b/packages/background-tips/README.md @@ -3,3 +3,21 @@ Displays tips about Pulsar in the background when there are no open editors. ![Screen shot](https://f.cloud.github.com/assets/69169/1796267/c3de038c-6a60-11e3-8bf8-36f45684902c.png) + +### Contributing tips + +Packages can contribute tips by adding a `backgroundTips` array to their `package.json`. Each entry is a string displayed as-is, with optional `{command}` placeholders that are replaced by the current keybinding for that command. + +```json +"backgroundTips": [ + "You can toggle the Tree View with {atom-workspace>tree-view:toggle}", + "You can open any file quickly using {atom-workspace>fuzzy-finder:toggle-file-finder}" +] +``` + +The placeholder syntax is `{command}` or `{scope>command}`: + +- `{command}` resolves the keybinding using the current platform. +- `{scope>command}` resolves the keybinding for the given CSS selector scope (e.g. `atom-workspace`, `atom-text-editor`, `body`). + +If the command in a placeholder has no keybinding defined, the tip is skipped entirely. diff --git a/packages/background-tips/lib/background-tips-view.js b/packages/background-tips/lib/background-tips-view.js index 0088d97c15..cc6d750fa3 100644 --- a/packages/background-tips/lib/background-tips-view.js +++ b/packages/background-tips/lib/background-tips-view.js @@ -1,52 +1,30 @@ -const _ = require("underscore-plus"); -const { CompositeDisposable, Disposable } = require("atom"); +const _ = require('underscore-plus'); +const { CompositeDisposable } = require('atom'); const TEMPLATE = `\
  • -
\ -`; +`; module.exports = class BackgroundTipsElement { - constructor(main) { - this.main = main; - this.element = document.createElement("background-tips"); + constructor() { + this.element = document.createElement('background-tips'); this.index = -1; this.workspaceCenter = atom.workspace.getCenter(); - this.startDelay = 1000; - this.displayDuration = 10000; this.fadeDuration = 300; - - this.renderedTips = []; - this.tipsRendered = false; - + this.tips = []; + this.tipSources = []; this.disposables = new CompositeDisposable(); - const visibilityCallback = () => this.updateVisibility(); - - this.disposables.add(this.workspaceCenter.onDidAddPane(visibilityCallback)); this.disposables.add( - this.workspaceCenter.onDidDestroyPane(visibilityCallback) + this.workspaceCenter.onDidAddPane(visibilityCallback), + this.workspaceCenter.onDidDestroyPane(visibilityCallback), + this.workspaceCenter.onDidChangeActivePaneItem(visibilityCallback), + atom.config.observe('background-tips.displayDuration', (value) => { + this.displayDuration = value * 1000; + }), ); - this.disposables.add( - this.workspaceCenter.onDidChangeActivePaneItem(visibilityCallback) - ); - - atom.getCurrentWindow().on("blur", visibilityCallback); - atom.getCurrentWindow().on("focus", visibilityCallback); - - this.disposables.add( - new Disposable(() => - atom.getCurrentWindow().removeListener("blur", visibilityCallback) - ) - ); - this.disposables.add( - new Disposable(() => - atom.getCurrentWindow().removeListener("focus", visibilityCallback) - ) - ); - this.startTimeout = setTimeout(() => this.start(), this.startDelay); } @@ -57,23 +35,17 @@ module.exports = class BackgroundTipsElement { attach() { this.element.innerHTML = TEMPLATE; - this.message = this.element.querySelector(".message"); - + this.message = this.element.querySelector('.message'); const paneView = atom.views.getView(this.workspaceCenter.getActivePane()); - const itemViews = paneView.querySelector(".item-views"); + const itemViews = paneView.querySelector('.item-views'); let top = 0; if (itemViews && itemViews.offsetTop) { top = itemViews.offsetTop; } - - this.element.style.top = top + "px"; + this.element.style.top = top + 'px'; paneView.appendChild(this.element); } - detach() { - this.element.remove(); - } - updateVisibility() { if (this.shouldBeAttached()) { this.start(); @@ -85,14 +57,13 @@ module.exports = class BackgroundTipsElement { shouldBeAttached() { return ( this.workspaceCenter.getPanes().length === 1 && - this.workspaceCenter.getActivePaneItem() == null && - atom.getCurrentWindow().isFocused() + this.workspaceCenter.getActivePaneItem() == null ); } start() { if (!this.shouldBeAttached() || this.interval != null) return; - this.renderTips(); + if (this.tips.length === 0) return; this.randomizeIndex(); this.attach(); this.showNextTip(); @@ -104,53 +75,82 @@ module.exports = class BackgroundTipsElement { if (this.interval != null) { clearInterval(this.interval); } - clearTimeout(this.startTimeout); clearTimeout(this.nextTipTimeout); this.interval = null; } randomizeIndex() { - const len = this.renderedTips.length; + const len = this.tips.length; this.index = len > 0 ? Math.round(Math.random() * len) % len : 0; } showNextTip() { - if (this.renderedTips.length === 0) return; - this.index = ++this.index % this.renderedTips.length; - this.message.classList.remove("fade-in"); + if (this.tips.length === 0) return; + let html = null; + for (let i = 0; i < this.tips.length; i++) { + this.index = (this.index + 1) % this.tips.length; + if (atom.packages.isPackageDisabled(this.tipSources[this.index])) continue; + html = this.renderTip(this.tips[this.index]); + if (html !== null) break; + this.log(`Skipping tip (missing keybinding): "${this.tips[this.index]}"`); + } + if (html === null) { + this.stop(); + return; + } + this.message.classList.remove('fade-in'); this.nextTipTimeout = setTimeout(() => { - this.message.innerHTML = this.renderedTips[this.index]; - this.message.classList.add("fade-in"); + this.log(`Displaying tip [${this.index + 1}/${this.tips.length}]: "${this.tips[this.index]}"`); + this.message.innerHTML = html; + this.message.classList.add('fade-in'); }, this.fadeDuration); } - renderTips() { - if (this.tipsRendered) return; - const tips = this.main.getTips(); - this.renderedTips = tips.map((tip) => this.renderTip(tip)); - this.tipsRendered = true; + addPackageTips(pkg) { + if (atom.packages.isPackageDisabled(pkg.name)) return; + const raw = pkg.metadata.backgroundTips; + if (!Array.isArray(raw) || raw.length === 0) return; + for (const tip of raw) { + this.tips.push(tip); + this.tipSources.push(pkg.name); + } + this.log(`Package "${pkg.name}" added ${raw.length} tip(s), total: ${this.tips.length}`); + if (this.interval == null) this.start(); } - tipsChanged() { - this.tipsRendered = false; + removePackageTips(pkg) { + const keep = []; + const keepSources = []; + for (let i = 0; i < this.tips.length; i++) { + if (this.tipSources[i] !== pkg.name) { + keep.push(this.tips[i]); + keepSources.push(this.tipSources[i]); + } + } + const removed = this.tips.length - keep.length; + if (removed === 0) return; + this.log(`Package "${pkg.name}" removed ${removed} tip(s), total: ${keep.length}`); + this.tips = keep; + this.tipSources = keepSources; if (this.interval != null) { - this.renderTips(); - this.index = Math.min(this.index, this.renderedTips.length - 1); + if (this.tips.length === 0) { + this.stop(); + } else { + this.index = Math.min(this.index, this.tips.length - 1); + } } } renderTip(str) { - str = str.replace(/\{(.+)\}/g, (match, command) => { + let missing = false; + const html = str.replace(/\{(.+)\}/g, (match, command) => { let binding, scope; - const scopeAndCommand = command.split(">"); + const scopeAndCommand = command.split('>'); if (scopeAndCommand.length > 1) { [scope, command] = scopeAndCommand; } - const bindings = atom.keymaps.findKeyBindings({ - command: command.trim(), - }); - + const bindings = atom.keymaps.findKeyBindings({ command: command.trim() }); if (scope) { for (binding of bindings) { if (binding.selector === scope) break; @@ -158,27 +158,30 @@ module.exports = class BackgroundTipsElement { } else { binding = this.getKeyBindingForCurrentPlatform(bindings); } - if (binding && binding.keystrokes) { - const keystrokeLabel = _.humanizeKeystroke(binding.keystrokes).replace( - /\s+/g, - " " - ); + const keystrokeLabel = _.humanizeKeystroke(binding.keystrokes).replace(/\s+/g, ' '); return `${keystrokeLabel}`; } else { - return command; + missing = true; + return ''; } }); - return str; + return missing ? null : html; } getKeyBindingForCurrentPlatform(bindings) { if (!bindings || !bindings.length) return; - for (let binding of bindings) { + for (const binding of bindings) { if (binding.selector.indexOf(process.platform) !== -1) { return binding; } } return bindings[0]; } + + log(...args) { + if (atom.config.get('background-tips.debug')) { + console.log('[background-tips]', ...args); + } + } }; diff --git a/packages/background-tips/lib/background-tips.js b/packages/background-tips/lib/background-tips.js index 8e7f0d325c..728ba27361 100644 --- a/packages/background-tips/lib/background-tips.js +++ b/packages/background-tips/lib/background-tips.js @@ -1,36 +1,25 @@ -const { Disposable } = require("atom"); +const { CompositeDisposable } = require("atom"); const BackgroundTipsView = require("./background-tips-view"); module.exports = { activate() { - this.defaultTips = require("./tips"); - this.addedTips = new Set(); - this.backgroundTipsView = new BackgroundTipsView(this); + this.backgroundTipsView = new BackgroundTipsView(); + this.disposables = new CompositeDisposable(); + for (let pkg of atom.packages.getLoadedPackages()) { + this.backgroundTipsView.addPackageTips(pkg); + } + this.disposables.add( + atom.packages.onDidLoadPackage((pkg) => { + this.backgroundTipsView.addPackageTips(pkg); + }), + atom.packages.onDidUnloadPackage((pkg) => { + this.backgroundTipsView.removePackageTips(pkg); + }), + ) }, deactivate() { + this.disposables.dispose(); this.backgroundTipsView.destroy(); - this.addedTips.clear(); - }, - - getTips() { - let all = [...this.defaultTips]; - for (const tips of this.addedTips) { - all = all.concat(tips); - } - return all; - }, - - provideBackgroundTips() { - return { - registerTips: (tips) => { - this.addedTips.add(tips); - this.backgroundTipsView.tipsChanged(); - return new Disposable(() => { - this.addedTips.delete(tips); - this.backgroundTipsView.tipsChanged(); - }); - }, - }; }, }; diff --git a/packages/background-tips/lib/tips.js b/packages/background-tips/lib/tips.js deleted file mode 100644 index 67d4619abf..0000000000 --- a/packages/background-tips/lib/tips.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = [ - "Split your editor into multiple panes with {pane:split-right-and-copy-active-item}", - "You can move lines of text up and down with {editor:move-line-up} and {editor:move-line-down}", - "Toggle line comments quickly with {editor:toggle-line-comments}", - // Tips for deferred packages that won't register via services until activated - `The Command Palette lets you access all of ${atom.branding.name}'s commands. Open it with {command-palette:toggle}`, - 'Dismiss panels like Find and Replace with {body>core:cancel}', -]; diff --git a/packages/background-tips/package.json b/packages/background-tips/package.json index 736d581c0f..511aada7c3 100644 --- a/packages/background-tips/package.json +++ b/packages/background-tips/package.json @@ -9,12 +9,25 @@ "engines": { "atom": ">0.42.0" }, - "providedServices": { - "background-tips": { - "description": "Allows packages to register tips shown when no editors are open", - "versions": { - "1.0.0": "provideBackgroundTips" - } + "backgroundTips": [ + "You can split your editor into multiple panes with {pane:split-right-and-copy-active-item}", + "You can move a line of text up with {atom-text-editor>editor:move-line-up}", + "You can move a line of text down with {atom-text-editor>editor:move-line-down}", + "You can toggle line comments with {atom-text-editor>editor:toggle-line-comments}" + ], + "configSchema": { + "displayDuration": { + "type": "integer", + "default": 10, + "minimum": 1, + "description": "How long each tip is displayed, in seconds.", + "order": 1 + }, + "debug": { + "type": "boolean", + "default": false, + "description": "Log debug information about tip collection and display to the console.", + "order": 2 } }, "dependencies": { diff --git a/packages/background-tips/spec/background-tips-spec.js b/packages/background-tips/spec/background-tips-spec.js index a967b6a0d3..7eba5ca21f 100644 --- a/packages/background-tips/spec/background-tips-spec.js +++ b/packages/background-tips/spec/background-tips-spec.js @@ -4,7 +4,6 @@ const { ffit, afterEach, beforeEach, - emitterEventPromise, } = require("./async-spec-helpers"); describe("BackgroundTips", () => { @@ -21,7 +20,6 @@ describe("BackgroundTips", () => { workspaceElement = atom.views.getView(atom.workspace); jasmine.attachToDOM(workspaceElement); jasmine.useMockClock(); - spyOn(atom.getCurrentWindow(), "isFocused").andReturn(true); }); describe("when the package is activated when there is only one pane", () => { @@ -114,77 +112,4 @@ describe("BackgroundTips", () => { }); }); - describe("provideBackgroundTips service", () => { - it("allows adding and removing tips", async () => { - const { mainModule } = await atom.packages.activatePackage( - "background-tips" - ); - const service = mainModule.provideBackgroundTips(); - - expect(typeof service.registerTips).toBe("function"); - - const originalCount = mainModule.getTips().length; - const disposable = service.registerTips(["Custom tip 1", "Custom tip 2"]); - expect(mainModule.getTips().length).toBe(originalCount + 2); - expect(mainModule.getTips()).toContain("Custom tip 1"); - expect(mainModule.getTips()).toContain("Custom tip 2"); - - disposable.dispose(); - expect(mainModule.getTips().length).toBe(originalCount); - expect(mainModule.getTips()).not.toContain("Custom tip 1"); - }); - - it("shows contributed tips when cycling", async () => { - const backgroundTipsView = await activatePackage(); - const { mainModule } = atom.packages.getActivePackage("background-tips"); - - const service = mainModule.provideBackgroundTips(); - service.registerTips(["Unique test tip for spec"]); - - advanceClock(backgroundTipsView.startDelay); - advanceClock(backgroundTipsView.fadeDuration); - - // The contributed tip should be in the rendered tips - expect( - backgroundTipsView.renderedTips.some((t) => - t.includes("Unique test tip for spec") - ) - ).toBe(true); - }); - }); - - describe("when Atom is not focused but all other requirements are satisfied", () => { - beforeEach(() => { - jasmine.unspy(atom.getCurrentWindow(), "isFocused"); - spyOn(atom.getCurrentWindow(), "isFocused").andReturn(false); - }); - - it("does not display the background tips", async () => { - expect(atom.workspace.getActivePane().getItems().length).toBe(0); - - const backgroundTipsView = await activatePackage(); - expect(backgroundTipsView.element.parentNode).toBeFalsy(); - advanceClock(backgroundTipsView.startDelay + 1); - expect(backgroundTipsView.element.parentNode).toBeFalsy(); - }); - - it("reactivates the background tips if the focus event is received", async () => { - expect(atom.workspace.getActivePane().getItems().length).toBe(0); - - const backgroundTipsView = await activatePackage(); - advanceClock(backgroundTipsView.startDelay + 1); - expect(backgroundTipsView.element.parentNode).toBeFalsy(); - - jasmine.unspy(atom.getCurrentWindow(), "isFocused"); - spyOn(atom.getCurrentWindow(), "isFocused").andReturn(true); - - const focusEvent = emitterEventPromise(atom.getCurrentWindow(), "focus"); - atom.getCurrentWindow().emit("focus"); // Manually emit to prevent actually blurring + refocusing the window - - await focusEvent; - - advanceClock(backgroundTipsView.startDelay + 1); - expect(backgroundTipsView.element.parentNode).toBeTruthy(); - }); - }); }); diff --git a/packages/background-tips/styles/background-tips.less b/packages/background-tips/styles/background-tips.less index d5a7781372..52d33029d6 100644 --- a/packages/background-tips/styles/background-tips.less +++ b/packages/background-tips/styles/background-tips.less @@ -1,7 +1,3 @@ -// The ui-variables file is provided by base themes provided by Pulsar. -// -// See https://github.com/pulsar-edit/pulsar/blob/master/packages/atom-dark-ui/styles/ui-variables.less -// for a full listing of what's available. @import "ui-variables"; background-tips { diff --git a/packages/command-palette/package.json b/packages/command-palette/package.json index f791b96b8e..4ec7b50ff8 100644 --- a/packages/command-palette/package.json +++ b/packages/command-palette/package.json @@ -24,6 +24,9 @@ "semver": "^5.4.1", "sinon": "^3.2.1" }, + "backgroundTips": [ + "Everything Pulsar can do is in the Command Palette. See it by using {atom-workspace>command-palette:toggle}" + ], "configSchema": { "preserveLastSearch": { "type": "boolean", diff --git a/packages/find-and-replace/package.json b/packages/find-and-replace/package.json index 1c06bab840..0697b395a0 100644 --- a/packages/find-and-replace/package.json +++ b/packages/find-and-replace/package.json @@ -40,6 +40,9 @@ "devDependencies": { "dedent": "^0.6.0" }, + "backgroundTips": [ + "Dismiss panels like Find and Replace with {body>core:cancel}" + ], "consumedServices": { "atom.file-icons": { "versions": { diff --git a/packages/fuzzy-finder/lib/main.js b/packages/fuzzy-finder/lib/main.js index 2bb1f7ba06..1a5aa055d1 100644 --- a/packages/fuzzy-finder/lib/main.js +++ b/packages/fuzzy-finder/lib/main.js @@ -77,14 +77,6 @@ module.exports = { return new Disposable(() => metricsReporter.unsetReporter()) }, - consumeBackgroundTips (service) { - const disposable = service.registerTips([ - 'Quickly open any file in your project with the Fuzzy Finder using {fuzzy-finder:toggle-file-finder}' - ]) - this.disposables.add(disposable) - return disposable - }, - serialize () { const paths = {} for (let editor of atom.workspace.getTextEditors()) { diff --git a/packages/fuzzy-finder/package.json b/packages/fuzzy-finder/package.json index 1fb4708352..183daa670c 100644 --- a/packages/fuzzy-finder/package.json +++ b/packages/fuzzy-finder/package.json @@ -41,13 +41,11 @@ "versions": { "^1.1.0": "consumeMetricsReporter" } - }, - "background-tips": { - "versions": { - "1.0.0": "consumeBackgroundTips" - } } }, + "backgroundTips": [ + "You can quickly open files with the Fuzzy Finder using {atom-workspace>fuzzy-finder:toggle-file-finder}" + ], "configSchema": { "ignoredNames": { "type": "array", diff --git a/packages/settings-view/lib/main.js b/packages/settings-view/lib/main.js index 64438fd99b..98781d3524 100644 --- a/packages/settings-view/lib/main.js +++ b/packages/settings-view/lib/main.js @@ -58,7 +58,6 @@ module.exports = { }, deactivate () { - this.backgroundTipsDisposable?.dispose() if (settingsView) settingsView.destroy() if (statusView) statusView.destroy() settingsView = null @@ -85,14 +84,6 @@ module.exports = { } }, - consumeBackgroundTips (service) { - const disposable = service.registerTips([ - 'Install packages, themes, and customize your editor in Settings {settings-view:open}' - ]) - this.backgroundTipsDisposable = disposable - return disposable - }, - consumeSnippets (snippets) { if (typeof snippets.getUnparsedSnippets === 'function') { SnippetsProvider.getSnippets = snippets.getUnparsedSnippets.bind(snippets) diff --git a/packages/settings-view/package.json b/packages/settings-view/package.json index 9db5539c50..00995c04ed 100644 --- a/packages/settings-view/package.json +++ b/packages/settings-view/package.json @@ -65,13 +65,11 @@ "versions": { "0.1.0": "consumeSnippets" } - }, - "background-tips": { - "versions": { - "1.0.0": "consumeBackgroundTips" - } } }, + "backgroundTips": [ + "You can install packages, themes, and customize your editor in Settings with {atom-workspace>settings-view:open}" + ], "deserializers": { "SettingsView": "createSettingsView" } diff --git a/packages/symbols-view/lib/main.js b/packages/symbols-view/lib/main.js index a61b3a2f89..a8aedeed78 100644 --- a/packages/symbols-view/lib/main.js +++ b/packages/symbols-view/lib/main.js @@ -111,21 +111,10 @@ module.exports = { this.broker?.destroy(); this.broker = null; - this.backgroundTipsDisposable?.dispose(); - this.backgroundTipsDisposable = null; - this.subscriptions?.dispose(); this.subscriptions = null; }, - consumeBackgroundTips (service) { - const disposable = service.registerTips([ - 'Navigate to any function or symbol in the current file with {symbols-view:toggle-file-symbols}' - ]) - this.backgroundTipsDisposable = disposable - return disposable - }, - consumeSymbolProvider(provider) { if (Array.isArray(provider)) { this.broker.add(...provider); diff --git a/packages/symbols-view/package.json b/packages/symbols-view/package.json index ef4526d2fa..5708bde1a1 100644 --- a/packages/symbols-view/package.json +++ b/packages/symbols-view/package.json @@ -68,13 +68,11 @@ "versions": { "1.0.0": "consumeSymbolProvider" } - }, - "background-tips": { - "versions": { - "1.0.0": "consumeBackgroundTips" - } } }, + "backgroundTips": [ + "You can jump to any function or symbol in the current file using {atom-workspace>symbols-view:toggle-file-symbols}" + ], "providedServices": { "hyperclick": { "versions": { diff --git a/packages/tree-view/lib/tree-view-package.js b/packages/tree-view/lib/tree-view-package.js index af0955e952..6709639a66 100644 --- a/packages/tree-view/lib/tree-view-package.js +++ b/packages/tree-view/lib/tree-view-package.js @@ -58,15 +58,6 @@ class TreeViewPackage { }) } - consumeBackgroundTips (service) { - const disposable = service.registerTips([ - 'Show or hide the Tree View with {tree-view:toggle}', - 'Jump to the Tree View without leaving your keyboard using {tree-view:toggle-focus}' - ]) - this.disposables.add(disposable) - return disposable - } - provideTreeView () { return { selectedPaths: () => this.getTreeViewInstance().selectedPaths(), diff --git a/packages/tree-view/package.json b/packages/tree-view/package.json index 559c29dd18..5253beb851 100644 --- a/packages/tree-view/package.json +++ b/packages/tree-view/package.json @@ -29,13 +29,12 @@ "versions": { "1.0.0": "consumeElementIcons" } - }, - "background-tips": { - "versions": { - "1.0.0": "consumeBackgroundTips" - } } }, + "backgroundTips": [ + "You can toggle the Tree View with {atom-workspace>tree-view:toggle}", + "You can focus the Tree View with {atom-workspace>tree-view:toggle-focus}" + ], "providedServices": { "tree-view": { "description": "A tree-like view of directories and files", From 6d3ed1380c42f3ee35df07bb75fb6eb3c4145df4 Mon Sep 17 00:00:00 2001 From: Sebastian Balcerowiak Date: Wed, 6 May 2026 20:07:52 +0200 Subject: [PATCH 6/7] Defer start until after initial delay --- packages/background-tips/lib/background-tips-view.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/background-tips/lib/background-tips-view.js b/packages/background-tips/lib/background-tips-view.js index cc6d750fa3..956e63682b 100644 --- a/packages/background-tips/lib/background-tips-view.js +++ b/packages/background-tips/lib/background-tips-view.js @@ -15,6 +15,7 @@ module.exports = class BackgroundTipsElement { this.fadeDuration = 300; this.tips = []; this.tipSources = []; + this.started = false; this.disposables = new CompositeDisposable(); const visibilityCallback = () => this.updateVisibility(); this.disposables.add( @@ -25,7 +26,7 @@ module.exports = class BackgroundTipsElement { this.displayDuration = value * 1000; }), ); - this.startTimeout = setTimeout(() => this.start(), this.startDelay); + this.startTimeout = setTimeout(() => { this.started = true; this.start(); }, this.startDelay); } destroy() { @@ -116,7 +117,7 @@ module.exports = class BackgroundTipsElement { this.tipSources.push(pkg.name); } this.log(`Package "${pkg.name}" added ${raw.length} tip(s), total: ${this.tips.length}`); - if (this.interval == null) this.start(); + if (this.started && this.interval == null) this.start(); } removePackageTips(pkg) { From cbae8993fe9855116dadabb1294c2081eda530a9 Mon Sep 17 00:00:00 2001 From: Sebastian Balcerowiak Date: Wed, 6 May 2026 21:02:28 +0200 Subject: [PATCH 7/7] Always collect tips from disabled packages --- packages/background-tips/lib/background-tips-view.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/background-tips/lib/background-tips-view.js b/packages/background-tips/lib/background-tips-view.js index 956e63682b..3263e46650 100644 --- a/packages/background-tips/lib/background-tips-view.js +++ b/packages/background-tips/lib/background-tips-view.js @@ -109,7 +109,6 @@ module.exports = class BackgroundTipsElement { } addPackageTips(pkg) { - if (atom.packages.isPackageDisabled(pkg.name)) return; const raw = pkg.metadata.backgroundTips; if (!Array.isArray(raw) || raw.length === 0) return; for (const tip of raw) {