Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Commit 72b3d3d

Browse files
authored
Merge pull request #277 from savetheclocktower/disabled-packages
Add the ability to disable a package’s snippets.
2 parents e9ed1ca + 6dbe2b1 commit 72b3d3d

File tree

3 files changed

+163
-10
lines changed

3 files changed

+163
-10
lines changed

lib/snippets.coffee

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,19 @@ module.exports =
1616
@loaded = false
1717
@userSnippetsPath = null
1818
@snippetIdCounter = 0
19+
@snippetsByPackage = new Map
1920
@parsedSnippetsById = new Map
2021
@editorMarkerLayers = new WeakMap
22+
2123
@scopedPropertyStore = new ScopedPropertyStore
24+
# The above ScopedPropertyStore will store the main registry of snippets.
25+
# But we need a separate ScopedPropertyStore for the snippets that come
26+
# from disabled packages. They're isolated so that they're not considered
27+
# as candidates when the user expands a prefix, but we still need the data
28+
# around so that the snippets provided by those packages can be shown in
29+
# the settings view.
30+
@disabledSnippetsScopedPropertyStore = new ScopedPropertyStore
31+
2232
@subscriptions = new CompositeDisposable
2333
@subscriptions.add atom.workspace.addOpener (uri) =>
2434
if uri is 'atom://.atom/snippets'
@@ -28,6 +38,9 @@ module.exports =
2838
@watchUserSnippets (watchDisposable) =>
2939
@subscriptions.add(watchDisposable)
3040

41+
@subscriptions.add atom.config.onDidChange 'core.packagesWithSnippetsDisabled', ({newValue, oldValue}) =>
42+
@handleDisabledPackagesDidChange(newValue, oldValue)
43+
3144
snippets = this
3245

3346
@subscriptions.add atom.commands.add 'atom-text-editor',
@@ -120,19 +133,74 @@ module.exports =
120133
else
121134
callback(new Disposable -> )
122135

136+
# Called when a user's snippets file is changed, deleted, or moved so that we
137+
# can immediately re-process the snippets it contains.
123138
handleUserSnippetsDidChange: ->
124139
userSnippetsPath = @getUserSnippetsPath()
125140
atom.config.transact =>
126141
@clearSnippetsForPath(userSnippetsPath)
127142
@loadSnippetsFile userSnippetsPath, (result) =>
128143
@add(userSnippetsPath, result)
129144

145+
# Called when the "Enable" checkbox is checked/unchecked in the Snippets
146+
# section of a package's settings view.
147+
handleDisabledPackagesDidChange: (newDisabledPackages, oldDisabledPackages) ->
148+
packagesToAdd = []
149+
packagesToRemove = []
150+
oldDisabledPackages ?= []
151+
newDisabledPackages ?= []
152+
for p in oldDisabledPackages
153+
packagesToAdd.push(p) unless newDisabledPackages.includes(p)
154+
155+
for p in newDisabledPackages
156+
packagesToRemove.push(p) unless oldDisabledPackages.includes(p)
157+
158+
atom.config.transact =>
159+
@removeSnippetsForPackage(p) for p in packagesToRemove
160+
@addSnippetsForPackage(p) for p in packagesToAdd
161+
162+
addSnippetsForPackage: (packageName) ->
163+
snippetSet = @snippetsByPackage.get(packageName)
164+
for filePath, snippetsBySelector of snippetSet
165+
@add(filePath, snippetsBySelector)
166+
167+
removeSnippetsForPackage: (packageName) ->
168+
snippetSet = @snippetsByPackage.get(packageName)
169+
# Copy these snippets to the "quarantined" ScopedPropertyStore so that they
170+
# remain present in the list of unparsed snippets reported to the settings
171+
# view.
172+
@addSnippetsInDisabledPackage(snippetSet)
173+
for filePath, snippetsBySelector of snippetSet
174+
@clearSnippetsForPath(filePath)
175+
130176
loadPackageSnippets: (callback) ->
131-
packages = atom.packages.getLoadedPackages()
132-
snippetsDirPaths = (path.join(pack.path, 'snippets') for pack in packages).sort (a, b) ->
133-
if /\/app\.asar\/node_modules\//.test(a) then -1 else 1
134-
async.map snippetsDirPaths, @loadSnippetsDirectory.bind(this), (error, results) ->
135-
callback(_.extend({}, results...))
177+
disabledPackageNames = atom.config.get('core.packagesWithSnippetsDisabled') or []
178+
packages = atom.packages.getLoadedPackages().sort (pack, b) ->
179+
if /\/app\.asar\/node_modules\//.test(pack.path) then -1 else 1
180+
181+
snippetsDirPaths = (path.join(pack.path, 'snippets') for pack in packages)
182+
183+
async.map snippetsDirPaths, @loadSnippetsDirectory.bind(this), (error, results) =>
184+
zipped = ({result: result, pack: packages[key]} for key, result of results)
185+
enabledPackages = []
186+
for o in zipped
187+
# Skip packages that contain no snippets.
188+
continue if Object.keys(o.result).length is 0
189+
# Keep track of which snippets come from which packages so we can
190+
# unload them selectively later. All packages get put into this map,
191+
# even disabled packages, because we need to know which snippets to add
192+
# if those packages are enabled again.
193+
@snippetsByPackage.set(o.pack.name, o.result)
194+
if disabledPackageNames.includes(o.pack.name)
195+
# Since disabled packages' snippets won't get added to the main
196+
# ScopedPropertyStore, we'll keep track of them in a separate
197+
# ScopedPropertyStore so that they can still be represented in the
198+
# settings view.
199+
@addSnippetsInDisabledPackage(o.result)
200+
else
201+
enabledPackages.push(o.result)
202+
203+
callback(_.extend({}, enabledPackages...))
136204

137205
doneLoading: ->
138206
@loaded = true
@@ -174,7 +242,7 @@ module.exports =
174242
atom.notifications.addError("Failed to load snippets from '#{filePath}'", {detail: error.message, dismissable: true})
175243
callback(object)
176244

177-
add: (filePath, snippetsBySelector) ->
245+
add: (filePath, snippetsBySelector, isDisabled = false) ->
178246
for selector, snippetsByName of snippetsBySelector
179247
unparsedSnippetsByPrefix = {}
180248
for name, attributes of snippetsByName
@@ -186,9 +254,13 @@ module.exports =
186254
else if not body?
187255
unparsedSnippetsByPrefix[prefix] = null
188256

189-
@storeUnparsedSnippets(unparsedSnippetsByPrefix, filePath, selector)
257+
@storeUnparsedSnippets(unparsedSnippetsByPrefix, filePath, selector, isDisabled)
190258
return
191259

260+
addSnippetsInDisabledPackage: (bundle) ->
261+
for filePath, snippetsBySelector of bundle
262+
@add(filePath, snippetsBySelector, true)
263+
192264
getScopeChain: (object) ->
193265
scopesArray = object?.getScopesArray?()
194266
scopesArray ?= object
@@ -198,10 +270,16 @@ module.exports =
198270
scope
199271
.join(' ')
200272

201-
storeUnparsedSnippets: (value, path, selector) ->
273+
storeUnparsedSnippets: (value, path, selector, isDisabled = false) ->
274+
# The `isDisabled` flag determines which scoped property store we'll use.
275+
# Active snippets get put into one and inactive snippets get put into
276+
# another. Only the first one gets consulted when we look up a snippet
277+
# prefix for expansion, but both stores have their contents exported when
278+
# the settings view asks for all available snippets.
202279
unparsedSnippets = {}
203280
unparsedSnippets[selector] = {"snippets": value}
204-
@scopedPropertyStore.addProperties(path, unparsedSnippets, priority: @priorityForSource(path))
281+
store = if isDisabled then @disabledSnippetsScopedPropertyStore else @scopedPropertyStore
282+
store.addProperties(path, unparsedSnippets, priority: @priorityForSource(path))
205283

206284
clearSnippetsForPath: (path) ->
207285
for scopeSelector of @scopedPropertyStore.propertiesForSource(path)
@@ -415,13 +493,28 @@ module.exports =
415493
new SnippetExpansion(snippet, editor, cursor, this)
416494

417495
getUnparsedSnippets: ->
418-
_.deepClone(@scopedPropertyStore.propertySets)
496+
results = []
497+
iterate = (sets) ->
498+
for item in sets
499+
newItem = _.deepClone(item)
500+
# The atom-slick library has already parsed the `selector` property, so
501+
# it's an AST here instead of a string. The object has a `toString`
502+
# method that turns it back into a string. That custom behavior won't
503+
# be preserved in the deep clone of the object, so we have to handle it
504+
# separately.
505+
newItem.selectorString = item.selector.toString()
506+
results.push(newItem)
507+
508+
iterate(@scopedPropertyStore.propertySets)
509+
iterate(@disabledSnippetsScopedPropertyStore.propertySets)
510+
results
419511

420512
provideSnippets: ->
421513
bundledSnippetsLoaded: => @loaded
422514
insertSnippet: @insert.bind(this)
423515
snippetsForScopes: @parsedSnippetsForScopes.bind(this)
424516
getUnparsedSnippets: @getUnparsedSnippets.bind(this)
517+
getUserSnippetsPath: @getUserSnippetsPath.bind(this)
425518

426519
onUndoOrRedo: (editor, isUndo) ->
427520
activeExpansions = @getExpansions(editor)

spec/fixtures/package-with-snippets/snippets/test.cson

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
leftLabelHTML: "<span style=\"color:red\">Label</span>"
1818
rightLabelHTML: "<span style=\"color:white\">Label</span>"
1919

20+
".package-with-snippets-unique-scope":
21+
"Test Snippet":
22+
prefix: "test"
23+
body: "testing 123"
24+
2025
".source.js":
2126
"Overrides a core package's snippet":
2227
prefix: "log"

spec/snippet-loading-spec.coffee

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,58 @@ describe "Snippet Loading", ->
211211
runs ->
212212
expect(console.warn).toHaveBeenCalled()
213213
expect(atom.notifications.addError).toHaveBeenCalled() if atom.notifications?
214+
215+
describe "packages-with-snippets-disabled feature", ->
216+
it "disables no snippets if the config option is empty", ->
217+
originalConfig = atom.config.get('core.packagesWithSnippetsDisabled')
218+
atom.config.set('core.packagesWithSnippetsDisabled', [])
219+
220+
activateSnippetsPackage()
221+
runs ->
222+
snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope'])
223+
expect(Object.keys(snippets).length).toBe 1
224+
atom.config.set('core.packagesWithSnippetsDisabled', originalConfig)
225+
226+
it "still includes a disabled package's snippets in the list of unparsed snippets", ->
227+
originalConfig = atom.config.get('core.packagesWithSnippetsDisabled')
228+
atom.config.set('core.packagesWithSnippetsDisabled', [])
229+
230+
activateSnippetsPackage()
231+
runs ->
232+
atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets'])
233+
allSnippets = snippetsService.getUnparsedSnippets()
234+
scopedSnippet = allSnippets.find (s) ->
235+
s.selectorString is '.package-with-snippets-unique-scope'
236+
expect(scopedSnippet).not.toBe undefined
237+
originalConfig = atom.config.get('core.packagesWithSnippetsDisabled')
238+
239+
it "never loads a package's snippets when that package is disabled in config", ->
240+
originalConfig = atom.config.get('core.packagesWithSnippetsDisabled')
241+
atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets'])
242+
243+
activateSnippetsPackage()
244+
runs ->
245+
snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope'])
246+
expect(Object.keys(snippets).length).toBe 0
247+
atom.config.set('core.packagesWithSnippetsDisabled', originalConfig)
248+
249+
it "unloads and/or reloads snippets from a package if the config option is changed after activation", ->
250+
originalConfig = atom.config.get('core.packagesWithSnippetsDisabled')
251+
atom.config.set('core.packagesWithSnippetsDisabled', [])
252+
253+
activateSnippetsPackage()
254+
runs ->
255+
snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope'])
256+
expect(Object.keys(snippets).length).toBe 1
257+
258+
# Disable it.
259+
atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets'])
260+
snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope'])
261+
expect(Object.keys(snippets).length).toBe 0
262+
263+
# Re-enable it.
264+
atom.config.set('core.packagesWithSnippetsDisabled', [])
265+
snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope'])
266+
expect(Object.keys(snippets).length).toBe 1
267+
268+
atom.config.set('core.packagesWithSnippetsDisabled', originalConfig)

0 commit comments

Comments
 (0)