Skip to content
This repository has been archived by the owner on Apr 1, 2020. It is now read-only.

Commit

Permalink
Feature: Prettier plugin (#1540)
Browse files Browse the repository at this point in the history
* Add initial implementation of prettier plugin

* add config options to test using config functionality

* update config to allow disabling, add status icon

* comment out console.log add check if code is formatted
add initial attempt at cursor position

* fix config to camelcase

* add assets for prettier icon

* add line column and [wip] attempt to return correct cursor pos

* fix cursor positioning

* Remove linecolumn, not working remove prettier asset
more work on cursor positioning
add success and error status icons

* update yarn lock

* Fix eslintrc add allowedFiletypes checking

* add onClick fn to prettier element

add allowedFiletypes option to config

* remove console.logs

* add convert offset function and get cursor offset function

* use vim based byte offset or prettier

* remove unused dependencies

* add install command or prettier plugin

* add install command to plugins command

* update dir or prettier plugin

* relocate prettier plugin to correct dir

* add filepath for parser inference and change icon

* remove extra CR added on joining string

* add printWidth option and add graphql to default fts

* fix print width use number

* add editor config option add
buffer state to track if modified add icon colors

* remove console.log

* conditionally render prettier status item

* minor change to rerun tests

* [WIP] Add prettier-plugin test

* [WIP] comment out test while attempting to test prettier plugin\

* comment out non-functional tests

* re-add commented tests, deactivate prettier test

* Tweak prettier plugin architecture for tests

* comment out ci tests and contiue work on prettier test

* fix config for test, update test [WIP]

* add functioning ci test for prettier

* ensure existence of prettier status item

* add helper functions to common
add new [wip] ci test

* refactor get cursor offset to a get method
make convertCursorPos zero base to march getCursorPos

* add new bufferCursorTest file

* fix missing CI test and remove redundant calc

* remove redundant methods from prettier test

* tweak function to keep state within function rather than globally
move failing windows test to bottom to see if only that test fails

* remove specific use of newline per platform

* switch to a more functional approach
slightly excessive for now but a foundation
for further tweaks in the ongoing development
of this plugin

* fix prettier callback to use new state tracking version
switch to function keyword to ensure applyprettier is hoisted
so callback can reference it

* fix typo and add check in prettier rc function

* use prettier info to poplulate compatible filetypes

* fix incorrect ref to config

* actually remove incorrect conffig ref
  • Loading branch information
akinsho authored Apr 6, 2018
1 parent 820af10 commit 3d21a97
Show file tree
Hide file tree
Showing 13 changed files with 465 additions and 21 deletions.
26 changes: 22 additions & 4 deletions browser/src/Editor/BufferManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export class Buffer implements IBuffer {
private _filePath: string
private _language: string
private _cursor: Oni.Cursor
private _cursorOffset: number
private _version: number
private _modified: boolean
private _lineCount: number
Expand All @@ -111,6 +112,10 @@ export class Buffer implements IBuffer {
return this._cursor
}

public get cursorOffset(): number {
return this._cursorOffset
}

public get version(): number {
return this._version
}
Expand Down Expand Up @@ -146,6 +151,18 @@ export class Buffer implements IBuffer {
this._actions.removeBufferLayer(parseInt(this._id, 10), layer)
}

/**
* convertOffsetToLineColumn
*/
public async convertOffsetToLineColumn(
cursorOffset = this._cursorOffset,
): Promise<types.Position> {
const line: number = await this._neovimInstance.callFunction("byte2line", [cursorOffset])
const countFromLine: number = await this._neovimInstance.callFunction("line2byte", [line])
const column = cursorOffset - countFromLine
return types.Position.create(line - 1, column)
}

public async getCursorPosition(): Promise<types.Position> {
const pos = await this._neovimInstance.callFunction("getpos", ["."])
const [, oneBasedLine, oneBasedColumn] = pos
Expand Down Expand Up @@ -213,10 +230,10 @@ export class Buffer implements IBuffer {
const bufferLinesPromise = this.getLines(0, 1024)
const detectIndentPromise = import("detect-indent")

await Promise.all([bufferLinesPromise, detectIndentPromise])

const bufferLines = await bufferLinesPromise
const detectIndent = await detectIndentPromise
const [bufferLines, detectIndent] = await Promise.all([
bufferLinesPromise,
detectIndentPromise,
])

const ret = detectIndent(bufferLines.join("\n"))

Expand Down Expand Up @@ -414,6 +431,7 @@ export class Buffer implements IBuffer {
this._version = evt.version
this._modified = evt.modified
this._lineCount = evt.bufferTotalLines
this._cursorOffset = evt.byte

this._cursor = {
line: evt.line - 1,
Expand Down
16 changes: 16 additions & 0 deletions browser/src/Services/Configuration/DefaultConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,22 @@ const BaseConfiguration: IConfigurationValues = {
"oni.status.git": 3,
},

"oni.plugins.prettier": {
settings: {
semi: false,
tabWidth: 2,
useTabs: false,
singleQuote: false,
trailingComma: "es5",
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: "avoid",
printWidth: 80,
},
formatOnSave: false,
enabled: false,
},

"tabs.mode": "tabs",
"tabs.height": "2.5em",
"tabs.highlight": true,
Expand Down
18 changes: 18 additions & 0 deletions browser/src/Services/Configuration/IConfigurationValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,24 @@ export interface IConfigurationValues {
"sidebar.marks.enabled": boolean
"sidebar.plugins.enabled": boolean

"oni.plugins.prettier": {
settings: {
semi: boolean
tabWidth: number
useTabs: boolean
singleQuote: boolean
trailingComma: "es5" | "all" | "none"
bracketSpacing: boolean
jsxBracketSameLine: boolean
arrowParens: "avoid" | "always"
printWidth: number
[key: string]: number | string | boolean
}
formatOnSave: boolean
enabled: boolean
allowedFiletypes?: string[]
}

"snippets.enabled": boolean
"snippets.userSnippetFolder": string

Expand Down
45 changes: 45 additions & 0 deletions extensions/oni-plugin-prettier/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"rules": {
"indent": 0,
"linebreak-style": ["off", "unix"],
"quotes": ["error", "double", { "allowTemplateLiterals": true }],
"semi": ["error", "never"],
"array-callback-return": "error",
"eqeqeq": "error",
"no-console": "on",
"strict": 0,
"no-unused-vars": 0,
"no-undef": 1,
"no-undefined": 1,
"camelcase": 1,
"no-underscore-dangle": 0,
"no-console": 0,
"no-invalid-this": 1,
"no-useless-return": 1,
"comma-dangle": ["error", "only-multiline"],
"comma-spacing": ["error", { "before": false, "after": true }]
},
"env": {
"es6": true,
"browser": true,
"jasmine": true,
"mocha": true
},
"settings": {
"ecmascript": 6,
"jsx": true
},
"extends": "eslint:recommended",
"parser": "babel-eslint",
"plugins": ["react"],
"globals": {
"_": false,
"remote": false,
"process": false,
"module": false,
"require": false,
"__dirname": false,
"preloadedData": false
},
"root": true
}
201 changes: 201 additions & 0 deletions extensions/oni-plugin-prettier/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
const path = require("path")
const prettier = require("prettier")

// Helper functions
const compose = (...fns) => argument => fns.reduceRight((arg, fn) => fn(arg), argument)
const joinOrSplit = (method, by = "\n") => array => array[method](by)
const join = joinOrSplit("join")
const split = joinOrSplit("split")
const isEqual = toCompare => initialItem => initialItem === toCompare
const isTrue = (...args) => args.every(a => Boolean(a))
const eitherOr = (...args) => args.find(a => !!a)
const flatten = multidimensional => [].concat(...multidimensional)

const isCompatible = (allowedFiletypes, defaultFiletypes) => filePath => {
const filetypes = isTrue(allowedFiletypes, Array.isArray(allowedFiletypes))
? allowedFiletypes
: defaultFiletypes
const extension = path.extname(filePath)
return filetypes.includes(extension)
}

const getSupportedLanguages = async () => {
const info = await prettier.getSupportInfo()
return flatten(info.languages.map(lang => lang.extensions))
}

const activate = async Oni => {
const config = Oni.configuration.getValue("oni.plugins.prettier")
const prettierItem = Oni.statusBar.createItem(0, "oni.plugins.prettier")

const applyPrettierWithState = applyPrettier()
const defaultFiletypes = await getSupportedLanguages()

const callback = async () => {
const isNormalMode = Oni.editors.activeEditor.mode === "normal"
if (isNormalMode) {
await applyPrettierWithState(Oni)
}
}
Oni.commands.registerCommand({
command: "autoformat.prettier",
name: "Autoformat with Prettier",
execute: callback,
})

const checkPrettierrc = async bufferPath => {
if (!bufferPath) {
throw new Error(`No buffer path passed for prettier to check for a Prettierrc`)
}
try {
return await prettier.resolveConfig(bufferPath)
} catch (e) {
throw new Error(`Error parsing config file, ${e}`)
}
}

// Status Bar Component ----
const { errorElement, successElement, prettierElement } = createPrettierComponent(Oni, callback)

prettierItem.setContents(prettierElement)

const setStatusBarContents = (statusBarItem, defaultElement) => async (
statusElement,
timeOut = 3500,
) => {
statusBarItem.setContents(statusElement)
await setTimeout(() => statusBarItem.setContents(defaultElement), timeOut)
}

const setPrettierStatus = setStatusBarContents(prettierItem, prettierElement)

function applyPrettier() {
// Track the buffer state within the function using a closure
// if the buffer as a string is the same as the last state
// do no format because nothing has changed
let lastBufferState = null

// pass in Oni explicitly - Make dependencies clearer
return async Oni => {
const { activeBuffer } = Oni.editors.activeEditor

const [arrayOfLines, { line, character }] = await Promise.all([
activeBuffer.getLines(),
activeBuffer.getCursorPosition(),
])

const hasNotChanged = compose(isEqual(lastBufferState), join)

if (hasNotChanged(arrayOfLines)) {
return
}

try {
const prettierrc = await checkPrettierrc(activeBuffer.filePath)
const prettierConfig = eitherOr(prettierrc, config.settings)

// Pass in the file path so prettier can infer the correct parser to use
const { formatted, cursorOffset } = prettier.formatWithCursor(
join(arrayOfLines),
Object.assign({ filepath: activeBuffer.filePath }, prettierConfig, {
cursorOffset: activeBuffer.cursorOffset,
}),
)
if (!formatted) {
throw new Error("Couldn't format the buffer")
}

await setPrettierStatus(successElement)

const withoutFinalCR = formatted.replace(/\n$/, "")
lastBufferState = withoutFinalCR

const [, { character, line }] = await Promise.all([
activeBuffer.setLines(0, arrayOfLines.length, split(withoutFinalCR)),
activeBuffer.convertOffsetToLineColumn(cursorOffset),
])

await activeBuffer.setCursorPosition(line, character)
await Oni.editors.activeEditor.neovim.command("w")
} catch (e) {
console.warn(`Couldn't format the buffer because: ${e}`)
await setPrettierStatus(errorElement)
}
}
}

const { allowedFiletypes, formatOnSave, enabled } = config
const checkCompatibility = isCompatible(allowedFiletypes, defaultFiletypes)

Oni.editors.activeEditor.onBufferEnter.subscribe(({ filePath }) => {
const hasCompatibility = checkCompatibility(filePath)

hasCompatibility ? prettierItem.show() : prettierItem.hide()
})

Oni.editors.activeEditor.onBufferSaved.subscribe(async ({ filePath }) => {
const hasCompatibility = checkCompatibility(filePath)

const canApplyPrettier = isTrue(formatOnSave, enabled, hasCompatibility)
if (canApplyPrettier) {
await applyPrettierWithState(Oni)
}
})
return { applyPrettier: applyPrettierWithState, checkCompatibility, checkPrettierrc }
}

function createPrettierComponent(Oni, onClick) {
const { React } = Oni.dependencies

const background = Oni.colors.getColor("highlight.mode.normal.background")
const foreground = Oni.colors.getColor("highlight.mode.normal.foreground")
const style = {
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
paddingLeft: "8px",
paddingRight: "8px",
color: "white",
backgroundColor: foreground,
}

const prettierIcon = (type = "magic") =>
Oni.ui.createIcon({
name: type,
size: Oni.ui.iconSize.Default,
})

const iconContainer = (type, color = "white") =>
React.createElement("div", { style: { padding: "0 6px 0 0", color } }, prettierIcon(type))

const prettierElement = React.createElement(
"div",
{ className: "prettier", style, onClick },
iconContainer(),
"prettier",
)

const errorElement = React.createElement(
"div",
{ style, className: "prettier" },
iconContainer("exclamation-triangle", "yellow"),
"prettier",
)

const successElement = React.createElement(
"div",
{ style, className: "prettier" },
iconContainer("check", "#5AB379"),
"prettier",
)

return {
errorElement,
prettierElement,
successElement,
}
}

module.exports = { activate }
18 changes: 18 additions & 0 deletions extensions/oni-plugin-prettier/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "oni-plugin-prettier",
"version": "1.0.0",
"main": "index.js",
"engines": {
"oni": "^0.2.6"
},
"scripts": {},
"oni": {
"supportedFileTypes": [
"*"
]
},
"dependencies": {
"prettier": "^1.11.1"
},
"devDependencies": {}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -838,8 +838,9 @@
"watch:plugins:oni-plugin-markdown-preview": "cd extensions/oni-plugin-markdown-preview && tsc --watch",
"uninstall-global": "npm rm -g oni-vim",
"install-global": "npm install -g oni-vim",
"install:plugins": "npm run install:plugins:oni-plugin-markdown-preview",
"install:plugins": "npm run install:plugins:oni-plugin-markdown-preview && npm run install:plugins:oni-plugin-prettier",
"install:plugins:oni-plugin-markdown-preview": "cd extensions/oni-plugin-markdown-preview && npm install --prod",
"install:plugins:oni-plugin-prettier": "cd extensions/oni-plugin-prettier && npm install --prod",
"postinstall": "npm run install:plugins && electron-rebuild && opencollective postinstall",
"profile:webpack": "webpack --config browser/webpack.production.config.js --profile --json > stats.json && webpack-bundle-analyzer browser/stats.json"
},
Expand Down
Loading

0 comments on commit 3d21a97

Please sign in to comment.