chocolate
Version:
A full stack Node.js web framework built using Coffeescript
811 lines (670 loc) • 33.5 kB
text/coffeescript
# **File** is a system module which offers services on Chocolate system files
#
# It provides :
#
# 1. a web editor interface to allow online source editing
# 2. web services to create and move source files
# 3. web services to interact with Git repository
Events = require 'events'
Fs = require 'fs'
Path = require 'path'
Util = require 'util'
# It uses **Git** to manage its system files versions
Git = require './git'
#### Public UI services
#### Access
# `access` service receives `where`, a system file path, and returns a web interface to edit the file.
exports.access = (where, backdoor_key, __) ->
# `backdoor_key` is a security key that gives access to all system services. If provided it is sent back in the editor.
# This service is asynchronous,
# so it returns an EventEmitter to which it will send an `end` message when it will be ready to send the web editor back.
event = new Events.EventEmitter
{where, path, error} = resolve where, __
if error? then process.nextTick(-> with_error event, error) ; return event
# First, get source file last modification date to pass it to the editor
exports.getModifiedDate(path).on 'end', (modifiedDate) ->
# Read the source file
Fs.readFile path, (err, data) ->
# Retrieve the commit list associated to this file from the Git repository
exports.getAvailableCommits(where, __).on 'end', (commits_list) ->
# Build commit Options list
commits = ''
for commit in commits_list
commits += (if commits is '' then """<option value="">Current working item - Last commit on : #{commit.date} </option>""" else '') + '<option value="' + commit.sha + '">' + commit.date + ' - ' + commit.message + '</option>'
commits = '<option value="">Current - Working item - Not commited' unless commits isnt ''
# And finally, parse the source and render it with our web editor interface with the help of the excellent Coffeekup.
# Send back the HTML.
source = data.toString().replace(/\&/g, '&').replace(/\</g, '<').replace(/\>/g, '>')
html = require('../general/coffeekup').render clientInterface, locals: {source, commits, where, backdoor_key, modifiedDate}
event.emit 'end', html
event
#### Public File services
#### hasWriteAccess
# `hasWriteAccess` checks if we have write access on path
exports.hasWriteAccess = (path) ->
return true if process.platform is 'win32' # temporary limitation on Windows platform
stats = Fs.statSync Path.resolve (if path? then path else '.')
canWrite = (owner, inGroup, mode) ->
return owner and (mode & 0x200) or # User is owner and owner can write.
inGroup and (mode & 0x20) or # User is in group and group can write.
(mode & 0x2) # Anyone can write.
return 0 isnt canWrite (process.getuid() is stats.uid), (process.getgid() is stats.gid), stats.mode
#### getModifiedDate
# `getModifiedDate` service receives `where`, a system file path, and returns the given file last modification date
exports.getModifiedDate = (where, __) ->
event = new Events.EventEmitter
# First, it tries to resolve the `where` parameter to a local path
{where, path, error} = resolve where, __
if error? then process.nextTick(-> with_error event, error) ; return event
# Then use the Fs `stat` service to get the file's last modification date
Fs.stat path, (error, stats) ->
return with_error event, error if error
event.emit 'end', Date.parse(stats.mtime)
event
#### getDirContent
# `getDirContent` returns the directory content as JSON string for the given `where` path ; resolve relative to process.cwd() or __.appdir if provided
exports.getDirContent = (where, __) ->
event = new Events.EventEmitter
result = []
{where, path, error} = resolve where, __
if error? then process.nextTick(-> with_error event, error) ; return event
Fs.readdir path, (error, files) ->
return with_error event, error if error
files = files.sort()
file_index = 0
get_file_stats = ->
if file_index < files.length
filename = files[file_index++]
Fs.stat path + '/' + filename, (error, stats) ->
return with_error event, error if error
result.push name:filename, isDir:stats.isDirectory(), isFile:stats.isFile(), extension:Path.extname(filename), modifiedDate: Date.parse(stats.mtime)
get_file_stats()
else
event.emit 'end', result
get_file_stats()
event
#### ensurePathExists
# `ensurePathExists` ensure that a path exists and creates directories if necessary; returns the path
exports.ensurePathExists = (path) ->
file_path = path.split('/')
file_name = file_path.pop()
file_pathname = ''
# Check the existence of every directory in the path
for path in file_path
if file_pathname is '' and path is '' then path = '/'
file_pathname += (if file_pathname != '' then '/' else '') + path
# Create the directory if non existent
if file_pathname isnt '' and not Fs.existsSync file_pathname
Fs.mkdirSync file_pathname, '755'
file_pathname += (if file_pathname isnt '' then '/' else '') + file_name
####
# `setFilenameSuffix` append a suffix to a filename before the extension
exports.setFilenameSuffix = (filename, suffix) ->
dirname = Path.dirname filename
if dirname is '.' then dirname = ''
if dirname isnt '' then dirname += '/'
ext = Path.extname filename
base = Path.basename filename, ext
dirname + base + suffix + ext
#### readDirDownSync
# `readDirDownSync` recursively read a directory and returns its content
exports.readDirDownSync = (dir, __) ->
dir = normalize(dir, check_suffix:no, __)
results = []
files = Fs.readdirSync dir
return results.sort() if files.length is 0
i = 0
next = () ->
file = files[i++]
unless file then return results.sort()
file = dir + '/' + file
stat = Fs.statSync file
if stat and stat.isDirectory()
results = results.concat exports.readDirDownSync file
next()
else
results.push file
next()
next()
#### writeToFile
# `writeToFile` write the provided data to a file. Directories and file are created if necessary
exports.writeToFile = (path, data, __) ->
event = new Events.EventEmitter
normalized_path = normalize path, __
if normalized_path is '' then process.nextTick(-> with_error event, 'Warning: empty path in writeToFile') ; return event
file_pathname = exports.ensurePathExists normalized_path
repo = resolve_repo normalized_path, __
# Create the file if non existent
unless Fs.existsSync file_pathname
Fs.open file_pathname, 'a', '644', (err, fd) ->
unless err?
Fs.close fd, (err) ->
# Add the file in Git index
new Git(cwd:repo.cwd).add(repo.path).on 'close', () ->
# Write the file content
Fs.writeFile file_pathname, data ? '', (err) ->
event.emit 'end', err
else
event.emit 'end', err
# Directly write file content if the file already exists
else
Fs.writeFile file_pathname, data ? '', (err) ->
event.emit 'end', err
event
#### moveFile
# `moveFile` move a file content from a path to another one
exports.moveFile = (from, to, __) ->
event = new Events.EventEmitter
from = normalize from, __
if from is '' then process.nextTick(-> with_error event, 'Warning: empty from in moveFile') ; return event
repo = resolve_repo from, __
# If file to move exists
if Fs.existsSync from
# Read its content
Fs.readFile from, (err, data) ->
unless err?
# Destroy the original file
Fs.unlink from, (err) ->
unless err?
# Remove it from the Git index
git = new Git cwd:repo.cwd
handler = git.add u:null, repo.path
handler.on 'close', () ->
# Move is a Remove if to is ''
return event.emit 'end', err if to is ''
# Create a file with a new name and the original content
handler = exports.writeToFile to, data, __
handler.on 'end', (err) ->
event.emit 'end', err
else
event.emit 'end', err
else
event.emit 'end', err
else
process.nextTick -> event.emit 'end', "Source file does not exists: #{from}"
event
#### removeFile
# `removeFile` remove a file
exports.removeFile = (path, __) ->
event = new Events.EventEmitter
path = normalize path, __
repo = resolve_repo path, __
# If file to move exists
if Fs.existsSync path
unless err?
# Destroy the original file
Fs.unlink path, (err) ->
unless err?
# Remove it from the Git index
git = new Git cwd:repo.cwd
handler = git.add u:null, repo.path
handler.on 'close', () ->
event.emit 'end', err
else
event.emit 'end', err
else
event.emit 'end', err
else
event.emit 'end', 'Source file does not exists'
event
#### load
# `load` load one system file
exports.load = (path, __) ->
event = new Events.EventEmitter
Fs.readFile require.resolve('../' + normalize path, __), (err, data) ->
event.emit 'end', data.toString()
event
#### grep
# `grep` look in file for a pattern; returns a list of filenames with last modif timestamp in seconds from EPOCH unless showdetails is specified
exports.grep = (pattern, with_case, show_details, __) ->
if typeof with_case is 'string' then with_case = with_case is 'true'
if typeof show_details is 'string' then show_details = show_details is 'true'
event = new Events.EventEmitter
results = []
suffixes = ['.coffee', '.js', '.css', '.scss', '.json', '.txt', '.md', '.markdown', '.ck', '.chockup', '.log' ]
if process.platform is 'win32'
params = ['/R', '/S', pattern, (__?.appdir ? '.') + '\\*']
unless with_case then params = ['/I'].concat params
unless show_details then params = ['/M'].concat params
else
#params = ['-r', '--include', "'*.coffee'", '--include', "'*.js'", '--include', "'*.css'", '--include', "'*.markdown'", '--include', "'*.md'", pattern, __?.appdir ? '.',]
params = ['-r'].concat(do -> s=[]; (s.push("--include"); s.push("'*#{suffix}'"); s) for suffix in suffixes; s).concat([pattern, __?.appdir ? '.',])
unless with_case then params = ['-i'].concat params
unless show_details then params = ['-l'].concat params else params = ['-n'].concat params
command = ([if process.platform is 'win32' then 'findstr' else 'grep'].concat params).join(' ')
if process.platform isnt 'win32' then unless show_details then command += " | while read line; do stat -c '%n %Y' $line; done"
grep = require('child_process').exec command
grep.stdout.on 'data', (data) ->
results = results.concat data.split(if process.platform is 'win32' then '\r\n' else '\n')
grep.stderr.on 'data', (data) ->
grep.on 'exit', (code) ->
results = results.filter (item) ->
[path] = item.toString().split ' '
Path.extname(path) in suffixes
results = results.map (item) ->
[path, stamp] = item.toString().split ' '
(Path.relative (__?.appdir ? '.'), path).replace(/\\/g, '/') + ' ' + stamp
event.emit 'end', results.join '\n'
event
#### Public Git services
#### commitToHistory
# `commitToHistory` commits current system files changes in Git repository
exports.commitToHistory = (message, repository, __) ->
event = new Events.EventEmitter
repo = resolve_repo repository, __
git = new Git cwd:repo.cwd
handler = git.commit a:null, m:message
handler.on 'close', () ->
event.emit 'end', 'Done'
event
#### loadFromHistory
# `loadFromHistory` load one system file from existing commit in Git repository
exports.loadFromHistory = (commit_sha, path, repo_cwd, __) ->
event = new Events.EventEmitter
repo = resolve_repo path, __
if repo_cwd isnt '' then repo.cwd = repo_cwd
git = new Git cwd:repo.cwd
answer = ''
handler = git.show '--raw', commit_sha + ':' + repo.path
handler.on 'item', (item) ->
answer += '' + item + '\n'
handler.on 'close', () ->
answer += handler.getBuffer().toString()
event.emit 'end', answer
event
#### getAvailableCommits
# `getAvailableCommits` return an html option string with available commits in Git for the provided `path`
exports.getAvailableCommits = (path, __) ->
commits = null
event = new Events.EventEmitter
repo = resolve_repo path, __
handler = new Git(cwd:repo.cwd).commits(follow:null,'name-status':null, repo.path)
handler.on 'item', (commit) ->
(commits ?= []).push sha:commit.sha, date:commit.author.date, message:commit.message, name:commit.name, repo:repo.cwd
handler.on 'close', ->
event.emit 'end', commits ? '[]'
event
#### getGitStatus
# `getGitStatus` return current Git status for current git repository
exports.getGitStatus = (path, __) ->
status = null
event = new Events.EventEmitter
repo = resolve_repo path, __
handler = new Git(cwd:repo.cwd).status()
handler.on 'item', (response) ->
(status ?= []).push response
handler.on 'close', ->
event.emit 'end', status ? '[]'
event
#### getFileDiff
# `getFileDiff` return current file Git diff
exports.getFileDiff = (path, __) ->
diff = null
event = new Events.EventEmitter
repo = resolve_repo path, __
handler = new Git(cwd:repo.cwd).diff(repo.path)
handler.on 'item', (response) ->
(diff ?= []).push response
handler.on 'close', ->
event.emit 'end', diff ? '[]'
event
#### logConsoleAndErrors
# `logConsoleAndErrors` filters process.stdout.write and process.stderr.write to write their output in a file. This covers the following use cases:
# process.stdout.write
# process.stderr.write
# console.log
# console.dir
# console.error
# someStream.pipe(process.stdout);
# throw new Error('Crash');
# throw 'never do this';
# throw undefined;
stdout_write = stderr_write = undefined
exports.logConsoleAndErrors = (path) ->
return if stdout_write?
stdout_write = process.stdout.write
stderr_write = process.stderr.write
write_stream = Fs.createWriteStream(path, { flags: 'a' })
log = (chunk) ->
chunk_ = switch
when typeof chunk is 'string' then chunk
when Buffer.isBuffer chunk then chunk.toString 'binary'
else chunk.toString()
write_stream.write chunk_
arguments[0] = chunk_
arguments
process.stdout.write = ((write) -> (chunk, encoding, fd) -> args = log.apply null, arguments ; write.apply process.stdout, args ; return)(process.stdout.write)
process.stderr.write = ((write) -> (chunk, encoding, fd) -> args = log.apply null, arguments ; write.apply process.stderr, args ; return)(process.stderr.write)
return
exports.unlogConsoleAndErrors = ->
return unless stdout_write?
process.stdout.write = stdout_write
process.stderr.write = stderr_write
return
exports.logWithTimestamp = ->
funcs =
log: console.log
info: console.info
warn: console.warn
error: console.error
timestamp = -> '[' + (new Date).toISOString() + ']'
Object.keys(funcs).forEach (k) ->
console[k] = ->
arguments0 = arguments[0]
arguments[0] = Util.format(timestamp(), arguments0)
funcs[k].apply console, arguments
return
return
#### Internal functions
#### normalize
# `normalize`
# - replace '-' by, or add sysdir value if appdir_relative and region is system
# - add __.appdir to the path if region isnt system and not appdir_relative
# - add .coffee extension to the provided `path` if it does not have one
normalize = (path, options, __) ->
appdir_relative = options?.appdir_relative
check_suffix = options?.check_suffix
if arguments.length is 2 and not appdir_relative? and not check_suffix? then __ = options ; options = null
appdir_relative ?= no
check_suffix ?= yes
path = path.trim()
if __?
is_system = no
if __.where.trim() is path and __.region is 'system'
is_system = yes
else
if (path.charAt(0) is '/' and path.charAt(1) is '-' and path.charAt(2) is '/') or (path.charAt(0) is '-' and path.charAt(1) is '/')
path = path.substr (if path[0] is '-' then 2 else 3)
is_system = yes
else
if (path.substr(0, __.sysdir.length + 2) is '/' + __.sysdir + '/') or (path.substr(0, __.sysdir.length + 1) is __.sysdir + '/')
path = path.substr(__.sysdir.length + (if path.charAt(0) is '/' then 2 else 1))
is_system = yes
if is_system
if appdir_relative
path = __.sysdir + (if path[0] isnt '/' then '/' else '') + path
else
if not appdir_relative
path = __.appdir + (if path[0] isnt '/' then '/' else '') + path
else
if (path.charAt(0) is '/' and path.charAt(1) is '-' and path.charAt(2) is '/') or (path.charAt(0) is '-' and path.charAt(1) is '/')
if appdir_relative then throw "Don't know relative path from sysdir to appdir"
else path = path.substr (if path[0] is '-' then 2 else 3)
if (not check_suffix) or (path is '') or Path.extname(path) isnt '' then path else path + '.coffee'
#### resolve
# `resolve` tries to resolve the `where` parameter to a local path
resolve = (where, __) ->
where_ = where
try path = Path.resolve normalize where, check_suffix:no, __
catch error then return {where, path, error}
unless Fs.existsSync path
path = normalize path
where = normalize where
unless Fs.existsSync path
path = ''
where = where_
error = "File '#{where} does not exists'"
{where, path, error}
#### resolve_repo
# `resolve_repo` adapts given path if in sysdir repository.
resolve_repo = (path, __) ->
appdir_abs = Path.resolve(__?.appdir ? '.')
switch path.charAt(0)
when '.'
git_dirname = Path.resolve path
git_path = if git_dirname.indexOf(appdir_abs) is 0 then git_dirname.substr appdir_abs.length + 1 else path
when '/'
git_path = path.substr 1
git_dirname = appdir_abs + path
else
git_path = path
path = '/' + path
git_dirname = appdir_abs + path
while not Fs.existsSync(git_dirname + (if git_dirname[git_dirname.length - 1] isnt '/' then '/' else '') + '.git')
new_dirname = Path.resolve(git_dirname, '..')
if new_dirname is git_dirname or new_dirname is appdir_abs
cwd = __?.appdir ? '.'
path = git_path
return {cwd, path}
else
git_dirname = new_dirname
{cwd:git_dirname, path:(appdir_abs + path).substr(git_dirname.length + 1)}
#### with_error
# `with_error` emits an `end` message containing the error message
with_error = (event, error) ->
event.emit 'end', {error}
#### clientInterface
# `clientInterface` holds the file web editor written with CoffeeKup in CoffeeScript
clientInterface = ->
doctype 5
html ->
head ->
meta
'http-equiv':'content-type'
'content':'text/html; charset:utf-8'
style
type:"text/css"
media:"screen"
"""
body {
overflow: hidden;
margin: 0;
background-color: lightgrey;
}
#ide {
visibility: hidden;
}
#toolbar {
}
#editor {
margin: 0;
position: absolute;
top: 30px;
bottom: 0;
left: 0;
right: 0;
font-size: 14px;
}
#editor * {
-webkit-overflow-scrolling: touch;
}
"""
body ->
script src:"/static/vendor/mootools/mootools-core.js", type:"text/javascript", charset:"utf-8"
script src:"/static/vendor/ace/ace.js", type:"text/javascript", charset:"utf-8"
script src:"/static/vendor/ace/mode-coffee.js", type:"text/javascript", charset:"utf-8"
script src:"/static/vendor/ace/mode-javascript.js", type:"text/javascript", charset:"utf-8"
script src:"/static/vendor/ace/mode-css.js", type:"text/javascript", charset:"utf-8"
script src:"/static/vendor/ace/mode-text.js", type:"text/javascript", charset:"utf-8"
script src:"/static/vendor/ace/mode-html.js", type:"text/javascript", charset:"utf-8"
script src:"/static/vendor/ace/mode-markdown.js", type:"text/javascript", charset:"utf-8"
script src:"/static/vendor/ace/theme-coffee.js", type:"text/javascript", charset:"utf-8"
div '#ide', ->
div '#toolbar', ->
input '#save_button',
type:'button'
value:'Save it'
onclick:'javascript:_ide.save()'
select '#commits',
onchange:'javascript:_ide.load()'
-> text commits
input '#commit_message',
type:'text'
input
type:'button'
value:'Commit'
onclick:'javascript:_ide.commit()'
div '#editor', -> text source
text '<script>_ide = {}; _sofkey = ' + (if backdoor_key isnt '' then '"' + backdoor_key + '"' else 'null') + '; _where = "' + where + '"; _modifiedDate = parseInt(' + (+modifiedDate) + ');</script>'
coffeescript ->
editor = null
codeMode = 'opened'
sofkey = _sofkey
where = _where
modifiedDate = _modifiedDate
_ide.load = ->
sha = document.id('commits').get 'value'
if sha is ''
url = (if sofkey? then '/!/' + sofkey else '') + '/-/server/file?sodowhat=load&path=' + where + '&how=raw'
else
url = (if sofkey? then '/!/' + sofkey else '') + '/-/server/file?sodowhat=loadFromHistory&commit_sha=' + sha + '&path=' + where + '.coffee' + '&how=raw'
new Request
url: url
onSuccess: (responseText) ->
# do not use responseText as it is filtered
editor.getSession().setValue @response.text
onFailure: (xhr) ->
alert 'Error: ' + xhr.status
.get()
_ide.save = ->
data = editor.getSession().getValue()
if data? and (data isnt '')
myRequest = new Request
url: (if sofkey? then '/!/' + sofkey else '') + '/-/' + where + '?so=move&how=raw'
onSuccess: (responseText) ->
modifiedDate = parseInt responseText
$('save_button').setStyle('color', '#000000')
onFailure: (xhr) ->
alert 'Error: ' + xhr.status
myRequest.post data
_ide.commit = ->
message = document.id('commit_message').get 'value'
new Request
url: (if sofkey? then '/!/' + sofkey else '') + '/-/server/file?sodowhat=commitToHistory&message=' + message + '&how=raw'
onSuccess: (responseText) ->
alert('File "' + where + '" commited. ' + responseText)
onFailure: (xhr) ->
alert('Error: ' + xhr.status)
.get();
_ide.toggleCodeMode = (mode, scope, size) ->
isCommentRow = (row) ->
tokens = editor.session.getTokens(row, row)[0].tokens
for token in tokens
if /^comment/.test(token.type)
return true
else
unless /^text/.test(token.type)
return false
false
toggleCode = (start, end) ->
range = editor.selection.getRange()
range.start.column = 0; range.start.row = start;
range.end.column = editor.session.doc.getLine(end - 1).length; range.end.row = end - 1
unless range.isEmpty() then editor.session.addFold '...', range
end + 1
if scope is 'full'
if codeMode is 'opened'
start = 0; end = 0; catched = false
for i in [0...editor.session.doc.getLength()]
if isCommentRow(i) is (mode is 'comment')
if (size is 'line') and catched
end += 1
else if end > start
start = end = toggleCode start, end
else
start = end += 1
catched = true
else
end += 1
catched = false
if end > start then toggleCode start, end
codeMode = 'closed'
else
range = editor.selection.getRange()
range.start.column = 0; range.start.row = 0
end = editor.session.doc.getLength() - 1
range.end.column = editor.session.doc.getLine(end).length; range.end.row = end
folds = editor.session.getFoldsInRange range
if folds.length > 0 then editor.session.removeFolds folds
codeMode = 'opened'
else
range = editor.selection.getRange()
start = range.start.row; end = start
fold = editor.session.getFoldAt range.start.row, range.start.column
if fold
if range.isEmpty() then editor.session.expandFold fold
else
if isCommentRow start
while isCommentRow(start) then start = end += 1
while (start >= 0) and not isCommentRow(start - 1) then start += -1
while (end + 1 < editor.session.doc.getLength()) and not isCommentRow(end + 1) then end += 1
if end + 1 > start
toggleCode start, end + 1
editor.selection.moveCursorTo start, 0
_ide.refresh_rate = 2 * 60 * 1000
_ide.get_extension = (path) ->
if (path = path.substr(1 + path.indexOf '/')).indexOf('.') is -1 then '' else '.' + path.split('.')[-1..][0]
keepUptodate = ->
new Request
url: (if sofkey? then '/!/' + sofkey else '') + '/-/server/file?sodowhat=getModifiedDate&path=' + where + '&how=raw'
onSuccess: (responseText) ->
if modifiedDate < parseInt responseText
window.location.reload()
onFailure: (xhr) ->
alert('Error: ' + xhr.status)
.get();
setTimeout keepUptodate, _ide.refresh_rate
window.addEvent 'domready', ->
mode = 'ace/mode/' + switch _ide.get_extension where ? ''
when '.coffee' then 'coffee'
when '.js' then 'javascript'
when '.json' then 'javascript'
when '.css' then 'css'
when '.html' then 'html'
when '.txt' then 'text'
when '.markdown', '.md' then 'markdown'
else 'coffee'
Mode = require(mode).Mode
set_editor = (id) ->
e = ace.edit id
try e.setScrollSpeed(2)
e.setShowPrintMargin false
setTimeout (-> e.setTheme 'ace/theme/coffee'; document.getElementById('ide').style.visibility = 'inherit'), 200
e.setShowInvisibles yes
e.getSession().setMode new Mode()
e.getSession().setUseSoftTabs true
e
editor = set_editor 'editor'
editor.getSession().on 'change', ->
button = $('save_button')
if button.getStyle('color') isnt 'red'
button.setStyle 'color', 'red'
true
editor.focus()
setTimeout keepUptodate, _ide.refresh_rate
commands = editor.commands
commands.addCommand
name: "save"
bindKey:
win: "Ctrl-S",mac: "Command-S"
exec: -> _ide.save()
commands.addCommand
name: "find next"
bindKey:
win: "F3|Ctrl-G"
exec: -> editor.findNext()
commands.addCommand
name: "find previous"
bindKey:
win: "Shift-F3|Shift-Ctrl-G"
exec: -> editor.findPrevious()
commands.addCommand
name: "toggle code mode"
bindKey:
win: "Alt-I", mac: "Alt-I"
exec: -> _ide.toggleCodeMode 'comment', 'one', 'block'
commands.addCommand
name: "toggle full one line code mode"
bindKey:
win: "Alt-Shift-U", mac: "Alt-Shift-U"
exec: -> _ide.toggleCodeMode 'comment', 'full', 'line'
commands.addCommand
name: "toggle full code mode"
bindKey:
win: "Alt-Shift-I", mac: "Alt-Shift-I"
exec: -> _ide.toggleCodeMode 'comment', 'full', 'block'
commands.addCommand
name: "toggle no comment mode"
bindKey:
win: "Alt-Shift-O", mac: "Alt-Shift-O"
exec: -> _ide.toggleCodeMode 'code', 'full', 'block'