diff --git a/lib/load-paths-handler.coffee b/lib/load-paths-handler.coffee new file mode 100644 index 0000000..6dcfbb7 --- /dev/null +++ b/lib/load-paths-handler.coffee @@ -0,0 +1,107 @@ +async = require 'async' +fs = require 'fs' +path = require 'path' +{GitRepository} = require 'atom' +{Minimatch} = require 'minimatch' + +PathsChunkSize = 10 +emittedPaths = new Set + +class PathLoader + constructor: (@rootPath, ignoreVcsIgnores, @traverseSymlinkDirectories, @ignoredNames, @extensions) -> + @paths = [] + @realPathCache = {} + @repo = null + if ignoreVcsIgnores + repo = GitRepository.open(@rootPath, refreshOnWindowFocus: false) + @repo = repo if repo?.relativize(path.join(@rootPath, 'test')) is 'test' + + load: (done) -> + @loadPath @rootPath, => + @flushPaths() + @repo?.destroy() + done() + + isIgnored: (loadedPath) -> + relativePath = path.relative(@rootPath, loadedPath) + if @repo?.isPathIgnored(relativePath) + true + else + for ignoredName in @ignoredNames + return true if ignoredName.match(relativePath) + + pathLoaded: (loadedPath, done) -> + badExtension = path.extname(loadedPath).toLowerCase() not in @extensions + unless @isIgnored(loadedPath) or emittedPaths.has(loadedPath) or badExtension + @paths.push(loadedPath) + emittedPaths.add(loadedPath) + + if @paths.length is PathsChunkSize + @flushPaths() + done() + + flushPaths: -> + emit('load-stylesheets:stylesheets-found', @paths) + @paths = [] + + loadPath: (pathToLoad, done) -> + return done() if @isIgnored(pathToLoad) + fs.lstat pathToLoad, (error, stats) => + return done() if error? + if stats.isSymbolicLink() + @isInternalSymlink pathToLoad, (isInternal) => + return done() if isInternal + fs.stat pathToLoad, (error, stats) => + return done() if error? + if stats.isFile() + @pathLoaded(pathToLoad, done) + else if stats.isDirectory() + if @traverseSymlinkDirectories + @loadFolder(pathToLoad, done) + else + done() + else + done() + else if stats.isDirectory() + @loadFolder(pathToLoad, done) + else if stats.isFile() + @pathLoaded(pathToLoad, done) + else + done() + + loadFolder: (folderPath, done) -> + fs.readdir folderPath, (error, children=[]) => + async.each( + children, + (childName, next) => + @loadPath(path.join(folderPath, childName), next) + done + ) + + isInternalSymlink: (pathToLoad, done) -> + fs.realpath pathToLoad, @realPathCache, (err, realPath) => + if err + done(false) + else + done(realPath.search(@rootPath) is 0) + +module.exports = (rootPaths, followSymlinks, ignoreVcsIgnores, ignores=[], extensions) -> + ignoredNames = [] + for ignore in ignores when ignore + try + ignoredNames.push(new Minimatch(ignore, matchBase: true, dot: true)) + catch error + console.warn "Error parsing ignore pattern (#{ignore}): #{error.message}" + + async.each( + rootPaths, + (rootPath, next) -> + new PathLoader( + rootPath, + ignoreVcsIgnores, + followSymlinks, + ignoredNames, + extensions + ).load(next) + @async() + ) diff --git a/lib/main.coffee b/lib/main.coffee index 2696398..78bbda8 100644 --- a/lib/main.coffee +++ b/lib/main.coffee @@ -3,4 +3,6 @@ provider = require './provider' module.exports = activate: -> provider.loadCompletions() + deactivate: -> provider.deactivate() + getProvider: -> provider diff --git a/lib/provider.coffee b/lib/provider.coffee index 08512a4..3741d80 100644 --- a/lib/provider.coffee +++ b/lib/provider.coffee @@ -1,5 +1,6 @@ fs = require 'fs' path = require 'path' +{CompositeDisposable, Task} = require 'atom' trailingWhitespace = /\s$/ attributePattern = /\s+([a-zA-Z][-a-zA-Z]*)\s*=\s*$/ @@ -8,6 +9,11 @@ tagPattern = /<([a-zA-Z][-a-zA-Z]*)(?:\s|$)/ module.exports = selector: '.text.html' disableForSelector: '.text.html .comment' + cssClassScope: 'entity.other.attribute-name.class.css' + cssIdScope: 'entity.other.attribute-name.id.css' + cssClassAttr: 'class' + cssIdAttr: 'id' + cssFileExtensions: ['.css', '.scss', '.less', '.html'] filterSuggestions: true getSuggestions: (request) -> @@ -133,7 +139,11 @@ module.exports = @buildAttributeValueCompletion(tag, attribute, value) buildAttributeValueCompletion: (tag, attribute, value) -> - if @completions.attributes[attribute].global + if attribute in [@cssClassAttr, @cssIdAttr] + text: value.value + type: attribute + description: "From #{atom.project.relativizePath(value.path)[1]}" + else if @completions.attributes[attribute].global text: value type: 'value' description: "#{value} value for global #{attribute} attribute" @@ -145,11 +155,20 @@ module.exports = descriptionMoreURL: @getLocalAttributeDocsURL(attribute, tag) loadCompletions: -> + @disposables = new CompositeDisposable @completions = {} + @cssCompletions = [] fs.readFile path.resolve(__dirname, '..', 'completions.json'), (error, content) => @completions = JSON.parse(content) unless error? return + atom.workspace.observeTextEditors (editor) => + @disposables.add editor.onDidSave (e) => + if path.extname(e.path).toLowerCase() in @cssFileExtensions + @cssCompletions = @cssCompletions.filter (c) -> c.path isnt e.path + @updateCSSCompletionsFromFile(e.path) + @pathLoader() + getPreviousTag: (editor, bufferPosition) -> {row} = bufferPosition while row >= 0 @@ -168,7 +187,40 @@ module.exports = attributePattern.exec(line)?[1] + updateCSSCompletionsFromFile: (fileName) -> + content = fs.readFileSync(fileName, 'utf-8') + grammar = atom.grammars.selectGrammar(fileName) + for line in grammar.tokenizeLines(content) + for token in line + [..., scope] = token.scopes + if scope in [@cssClassScope, @cssIdScope] + @cssCompletions.push + path: fileName + scope: scope + value: token.value + + pathLoader: -> + fileNames = [] + + followSymlinks = atom.config.get 'core.followSymlinks' + ignoredNames = atom.config.get('core.ignoredNames') ? [] + ignoreVcsIgnores = atom.config.get('core.excludeVcsIgnoredPaths') + + taskPath = require.resolve('./load-paths-handler') + + task = Task.once taskPath, atom.project.getPaths(), followSymlinks, + ignoreVcsIgnores, ignoredNames, @cssFileExtensions, => + for f in fileNames + @updateCSSCompletionsFromFile(f) + + task.on 'load-stylesheets:stylesheets-found', (paths) -> + fileNames.push(paths...) + getAttributeValues: (attribute) -> + if attribute?.toLowerCase() is @cssClassAttr + return (c for c in @cssCompletions when c.scope is @cssClassScope) + else if attribute?.toLowerCase() is @cssIdAttr + return (c for c in @cssCompletions when c.scope is @cssIdScope) attribute = @completions.attributes[attribute] attribute?.attribOption ? [] @@ -184,5 +236,9 @@ module.exports = getGlobalAttributeDocsURL: (attribute) -> "https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/#{attribute}" + deactivate: -> + @disposables.dispose() + firstCharsEqual = (str1, str2) -> + str1 = str1?.value or str1 str1[0].toLowerCase() is str2[0].toLowerCase() diff --git a/package.json b/package.json index 21c84c0..2b60d02 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,11 @@ }, "devDependencies": { "coffeelint": "^1.9.7", - "request": "^2.53.0" + "request": "^2.53.0", + "temp": "^0.8.3" + }, + "dependencies": { + "async": "^1.5.1", + "minimatch": "^3.0.0" } } diff --git a/spec/provider-spec.coffee b/spec/provider-spec.coffee index 883b3b1..5624155 100644 --- a/spec/provider-spec.coffee +++ b/spec/provider-spec.coffee @@ -1,3 +1,7 @@ +fs = require 'fs' +path = require 'path' +temp = require 'temp' + describe "HTML autocompletions", -> [editor, provider] = [] @@ -283,3 +287,56 @@ describe "HTML autocompletions", -> args = atom.commands.dispatch.mostRecentCall.args expect(args[0].tagName.toLowerCase()).toBe 'atom-text-editor' expect(args[1]).toBe 'autocomplete-plus:activate' + +describe "CSS completions inside of HTML attributes", -> + [editor, provider] = [] + + getCompletions = -> + cursor = editor.getLastCursor() + start = cursor.getBeginningOfCurrentWordBufferPosition() + end = cursor.getBufferPosition() + prefix = editor.getTextInRange([start, end]) + request = + editor: editor + bufferPosition: end + scopeDescriptor: cursor.getScopeDescriptor() + prefix: prefix + provider.getSuggestions(request) + + beforeEach -> + waitsForPromise -> atom.packages.activatePackage('autocomplete-html') + waitsForPromise -> atom.packages.activatePackage('language-html') + waitsForPromise -> atom.packages.activatePackage('language-css') + + runs -> + provider = atom.packages.getActivePackage('autocomplete-html').mainModule.getProvider() + + projectDir = fs.realpathSync(temp.mkdirSync('atom-project')) + samplePath = path.join(projectDir, 'sample.html') + fs.writeFileSync(samplePath, """ + +