// // Entry point. Builds the store, the four sidebar sections, and the command // handlers, then keeps the sections in step with the store. // const artifacts = require("./controllers/artifacts.js"); const auth = require("./controllers/auth.js"); const browser = require("./controllers/browser.js"); const log = require("./util/log.js"); const logs = require("./controllers/logs.js"); const pullRequests = require("./controllers/pullRequests.js"); const review = require("./controllers/review.js"); const runControl = require("./controllers/runControl.js"); const secretsVariables = require("./controllers/secretsVariables.js"); const selection = require("./controllers/selection.js"); const ui = require("./ui.js"); const { BranchProvider } = require("./views/branchView.js"); const { PullRequestsProvider } = require("./views/pullRequestsView.js"); const { ReviewCommentsController } = require("./controllers/reviewComments.js"); const { SettingsProvider } = require("./views/settingsView.js"); const { Store } = require("./store.js"); const { WorkflowsProvider } = require("./views/workflowsView.js"); const RELOAD_DEBOUNCE_MS = 150; // Preferences that change which repositories exist, rather than only how they // are displayed, force a full rediscovery. const REDISCOVERY_KEYS = [ "gitea.baseUrl", "gitea.instances", "gitea.hostAliases", "gitea.discovery.mode", ]; const DISPLAY_KEYS = [ "gitea.maxRunsPerRepo", "gitea.maxJobsPerRun", "gitea.branchFilter", "gitea.branchFilter.custom", ]; let state = null; function buildSections(store) { const definitions = [ { id: "gitea.branch", provider: new BranchProvider(store) }, { id: "gitea.workflows", provider: new WorkflowsProvider(store) }, { id: "gitea.pullRequests", provider: new PullRequestsProvider(store) }, { id: "gitea.settings", provider: new SettingsProvider(store) }, ]; return definitions.map((definition) => { const view = new TreeView(definition.id, { dataProvider: definition.provider }); nova.subscriptions.add(view); const section = Object.assign(definition, { view: view }); trackExpansion(section); return section; }); } /** Rebuilds every section from current store state. */ function reloadSections(sections) { for (const section of sections) { section.provider.invalidate(); section.view.reload().catch((error) => log.debug("reload failed", String(error))); } } /** * Mirrors expansion back into the provider. Nova applies whatever collapsible * state getTreeItem returns, so a reload closes anything the provider does not * know is open. */ function trackExpansion(section) { const expand = section.view.onDidExpandElement((element) => section.provider.setExpanded(element, true), ); const collapse = section.view.onDidCollapseElement((element) => section.provider.setExpanded(element, false), ); for (const subscription of [expand, collapse]) { if (subscription) nova.subscriptions.add(subscription); } } function registerCommands(store, refresh, reviewComments, redraw) { auth.register(store, refresh); logs.register(store); artifacts.register(store); runControl.register(store, refresh); pullRequests.register(store, refresh); review.register(store, refresh, reviewComments); secretsVariables.register(store); browser.register(); nova.commands.register("gitea.refresh", async () => { // An explicit refresh always redraws, even if nothing came back // different, so the command visibly does something. redraw(); await refresh({ rediscover: true }); await reviewComments.reload({ force: true }); }); // The per-section refresh buttons all drive the same fetch; they exist so // each section header has an affordance of its own. for (const name of ["gitea.refreshRuns", "gitea.refreshPullRequests", "gitea.refreshSettings"]) { nova.commands.register(name, () => refresh()); } nova.commands.register("gitea.refreshRepo", async (argument) => { const repo = await selection.resolveRepo(argument, { prompt: "Refresh which repository?", }); if (repo) await store.refreshRepo(repo.key); }); } function watchPreferences(refresh) { const subscriptions = []; for (const key of REDISCOVERY_KEYS) { subscriptions.push(nova.config.onDidChange(key, () => refresh({ rediscover: true }))); } for (const key of DISPLAY_KEYS) { subscriptions.push(nova.config.onDidChange(key, () => refresh())); if (nova.workspace && nova.workspace.config) { subscriptions.push(nova.workspace.config.onDidChange(key, () => refresh())); } } if (nova.workspace && nova.workspace.config) { subscriptions.push( nova.workspace.config.onDidChange("gitea.baseUrl", () => refresh({ rediscover: true }), ), ); } for (const subscription of subscriptions) { if (subscription) nova.subscriptions.add(subscription); } } exports.activate = function () { const store = new Store(); const sections = buildSections(store); const reviewComments = new ReviewCommentsController(store); selection.configure( store, sections.map((section) => section.view), ); let timer = null; let lastSignature = null; /** Drops the memo so the next change redraws unconditionally. */ const redraw = () => { lastSignature = null; }; const scheduleReload = () => { if (timer) clearTimeout(timer); timer = setTimeout(() => { timer = null; // Reloading rebuilds every row, which is disruptive even with // expansion preserved, so a poll that changed nothing does nothing. const signature = store.signature(); if (signature === lastSignature) { log.debug("nothing changed; leaving the sidebar alone"); return; } lastSignature = signature; reloadSections(sections); }, RELOAD_DEBOUNCE_MS); }; const subscription = store.onDidChange(scheduleReload); const refresh = (options) => store.refresh(options).catch((error) => { log.error("refresh failed", String(error)); }); registerCommands(store, refresh, reviewComments, redraw); watchPreferences(refresh); state = { store: store, sections: sections, reviewComments: reviewComments, subscription: subscription, cancelReload: () => { if (timer) clearTimeout(timer); }, }; log.info(`activated for ${store.router.baseUrls.join(", ") || "no configured instance"}`); if (!store.router.baseUrls.length) { ui.warn("Gitea: set an instance URL in the extension preferences to get started."); return; } refresh({ rediscover: true }).then(() => reviewComments.reload({ force: true })); }; exports.deactivate = function () { if (!state) return; state.cancelReload(); state.subscription.dispose(); state.reviewComments.dispose(); state.store.dispose(); state = null; };