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

[WIP] Complete CSS selectors inside of attributes #21

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions lib/load-paths-handler.coffee
Original file line number Diff line number Diff line change
@@ -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()
)
2 changes: 2 additions & 0 deletions lib/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ provider = require './provider'
module.exports =
activate: -> provider.loadCompletions()

deactivate: -> provider.deactivate()

getProvider: -> provider
58 changes: 57 additions & 1 deletion lib/provider.coffee
Original file line number Diff line number Diff line change
@@ -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*$/
Expand All @@ -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) ->
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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 ? []

Expand All @@ -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()
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
57 changes: 57 additions & 0 deletions spec/provider-spec.coffee
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
fs = require 'fs'
path = require 'path'
temp = require 'temp'

describe "HTML autocompletions", ->
[editor, provider] = []

Expand Down Expand Up @@ -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, """<html>
<style>
#test1 {}
.test2 {
font-size: 15px;
}
</style>
<div class="
<div id="
</html>""")

atom.project.setPaths([projectDir])
waitsForPromise -> atom.workspace.open(samplePath)
waitsFor -> provider.cssCompletions.length > 0
runs -> editor = atom.workspace.getActiveTextEditor()

it "autocompletes class names within open file", ->
editor.setCursorBufferPosition([7, 12])
completions = getCompletions()
expect(completions.length).toBe 1
expect(completions[0].text).toBe 'test2'

it "autocompletes ids within open file", ->
editor.setCursorBufferPosition([8, 9])
completions = getCompletions()
expect(completions.length).toBe 1
expect(completions[0].text).toBe 'test1'