@@ -7,15 +7,15 @@ import type {
7
7
RangeCodeFormatProvider ,
8
8
} from "./types"
9
9
10
- import { registerOnWillSave } from "@atom-ide-community/nuclide-commons-atom/FileEventHandlers"
11
10
import { getFormatOnSave , getFormatOnType } from "./config"
12
11
import { getLogger } from "log4js"
13
12
import { ProviderRegistry } from "atom-ide-base/commons-atom/ProviderRegistry"
14
13
import { applyTextEditsToBuffer } from "@atom-ide-community/nuclide-commons-atom/text-edit"
15
14
import nuclideUri from "@atom-ide-community/nuclide-commons/nuclideUri"
15
+ import { debounce } from "./utils"
16
16
17
17
// Save events are critical, so don't allow providers to block them.
18
- export const SAVE_TIMEOUT = 2500
18
+ export const SAVE_TIMEOUT = 500
19
19
20
20
export default class CodeFormatManager {
21
21
_subscriptions : CompositeDisposable
@@ -49,28 +49,36 @@ export default class CodeFormatManager {
49
49
}
50
50
} ) ,
51
51
52
- // Events from typing in the editor
53
52
atom . workspace . observeTextEditors ( ( editor ) => {
54
- const onChangeSubs = editor . getBuffer ( ) . onDidStopChanging ( async ( event ) => {
55
- try {
56
- await this . _formatCodeOnTypeInTextEditor ( editor , event )
57
- } catch ( err ) {
58
- getLogger ( "code-format" ) . warn ( "Failed to format code on type:" , err )
59
- }
60
- } )
53
+ const editorSubs = new CompositeDisposable (
54
+ // Format on typing in the editor
55
+ editor . getBuffer ( ) . onDidStopChanging ( async ( event ) => {
56
+ if ( ! getFormatOnType ( ) ) {
57
+ return
58
+ }
59
+ try {
60
+ await this . _formatCodeOnTypeInTextEditor ( editor , event )
61
+ } catch ( err ) {
62
+ getLogger ( "code-format" ) . warn ( "Failed to format code on type:" , err )
63
+ }
64
+ } ) ,
65
+ // Format on save
66
+ editor . onDidSave (
67
+ debounce ( async ( ) => {
68
+ const edits = await this . _formatCodeOnSaveInTextEditor ( editor )
69
+ const success = applyTextEditsToBuffer ( editor . getBuffer ( ) , edits ) as boolean
70
+ if ( ! success ) {
71
+ throw new Error ( "No code formatting providers found!" )
72
+ }
73
+ } , SAVE_TIMEOUT )
74
+ )
75
+ )
61
76
// Make sure we halt everything when the editor gets destroyed.
62
77
// We need to capture when editors are about to be destroyed in order to
63
78
// interrupt any pending formatting operations. (Otherwise, we may end up
64
79
// attempting to save a destroyed editor!)
65
- editor . onDidDestroy ( ( ) => onChangeSubs . dispose ( ) )
66
- } ) ,
67
-
68
- // Format on save
69
- registerOnWillSave ( ( ) => ( {
70
- priority : 0 ,
71
- timeout : SAVE_TIMEOUT ,
72
- callback : this . _formatCodeOnSaveInTextEditor . bind ( this ) ,
73
- } ) )
80
+ editor . onDidDestroy ( ( ) => editorSubs . dispose ( ) )
81
+ } )
74
82
)
75
83
76
84
this . _rangeProviders = new ProviderRegistry ( )
@@ -80,15 +88,19 @@ export default class CodeFormatManager {
80
88
}
81
89
82
90
// Return the text edits used to format code in the editor specified.
83
- async _formatCodeInTextEditor ( editor : TextEditor , range ?: Range ) : Promise < Array < TextEdit > > {
91
+ async _formatCodeInTextEditor (
92
+ editor : TextEditor ,
93
+ selectionRange : Range = editor . getSelectedBufferRange ( )
94
+ ) : Promise < Array < TextEdit > > {
84
95
const buffer = editor . getBuffer ( )
85
- const selectionRange = range ?? editor . getSelectedBufferRange ( )
86
- const { start : selectionStart , end : selectionEnd } = selectionRange
96
+ const bufferRange = buffer . getRange ( )
97
+
87
98
let formatRange : Range
88
99
if ( selectionRange . isEmpty ( ) ) {
89
100
// If no selection is done, then, the whole file is wanted to be formatted.
90
- formatRange = buffer . getRange ( )
101
+ formatRange = bufferRange
91
102
} else {
103
+ const { start : selectionStart , end : selectionEnd } = selectionRange
92
104
// Format selections should start at the beginning of the line,
93
105
// and include the last selected line end.
94
106
// (If the user has already selected complete rows, then depending on how they
@@ -101,60 +113,64 @@ export default class CodeFormatManager {
101
113
selectionEnd . column === 0 ? selectionEnd : [ selectionEnd . row + 1 , 0 ]
102
114
)
103
115
}
104
- const rangeProviders = [ ...this . _rangeProviders . getAllProvidersForEditor ( editor ) ]
105
- const fileProviders = [ ...this . _fileProviders . getAllProvidersForEditor ( editor ) ]
106
- const contents = editor . getText ( )
107
116
108
- const allEdits = await this . _reportBusy (
117
+ // range providers
118
+ const rangeProviders = [ ...this . _rangeProviders . getAllProvidersForEditor ( editor ) ]
119
+ const allRangeEdits = await this . _reportBusy (
109
120
editor ,
110
121
Promise . all ( rangeProviders . map ( ( p ) => p . formatCode ( editor , formatRange ) ) )
111
122
)
112
- const rangeEdits = allEdits . filter ( ( edits ) => edits . length > 0 )
123
+ const rangeEdits = allRangeEdits . filter ( ( edits ) => edits . length > 0 )
113
124
114
- const allResults = await this . _reportBusy (
125
+ // file providers
126
+ const fileProviders = [ ...this . _fileProviders . getAllProvidersForEditor ( editor ) ]
127
+ const allFileEdits = await this . _reportBusy (
115
128
editor ,
116
129
Promise . all ( fileProviders . map ( ( p ) => p . formatEntireFile ( editor , formatRange ) ) )
117
130
)
118
- const nonNullResults = allResults . filter ( ( result ) => result !== null && result !== undefined ) as {
131
+ const nonNullFileEdits = allFileEdits . filter ( ( result ) => result !== null && result !== undefined ) as {
119
132
newCursor ?: number
120
133
formatted : string
121
134
} [ ]
122
- const fileEdits = nonNullResults . map ( ( { formatted } ) => [
135
+ const contents = editor . getText ( )
136
+ const editorRange = editor . getBuffer ( ) . getRange ( )
137
+ const fileEdits = nonNullFileEdits . map ( ( { formatted } ) => [
123
138
{
124
- oldRange : editor . getBuffer ( ) . getRange ( ) ,
139
+ oldRange : editorRange ,
125
140
newText : formatted ,
126
141
oldText : contents ,
127
142
} as TextEdit ,
128
143
] )
129
144
145
+ // merge edits
130
146
// When formatting the entire file, prefer file-based providers.
131
- const preferFileEdits = formatRange . isEqual ( buffer . getRange ( ) )
147
+ const preferFileEdits = formatRange . isEqual ( bufferRange )
132
148
const edits = preferFileEdits ? fileEdits . concat ( rangeEdits ) : rangeEdits . concat ( fileEdits )
133
149
return edits . flat ( ) // TODO or [0]?
134
150
}
135
151
136
152
async _formatCodeOnTypeInTextEditor (
137
153
editor : TextEditor ,
138
- aggregatedEvent : BufferStoppedChangingEvent
154
+ { changes } : BufferStoppedChangingEvent
139
155
) : Promise < Array < TextEdit > > {
140
156
// Don't try to format changes with multiple cursors.
141
- if ( aggregatedEvent . changes . length !== 1 ) {
157
+ if ( changes . length !== 1 ) {
158
+ return [ ]
159
+ }
160
+ // if no provider return immediately
161
+ const providers = [ ...this . _onTypeProviders . getAllProvidersForEditor ( editor ) ]
162
+ if ( providers . length === 0 ) {
142
163
return [ ]
143
164
}
144
- const event = aggregatedEvent . changes [ 0 ]
165
+ const event = changes [ 0 ]
145
166
// This also ensures the non-emptiness of event.newText for below.
146
- if ( ! shouldFormatOnType ( event ) || ! getFormatOnType ( ) ) {
167
+ if ( ! shouldFormatOnType ( event ) ) {
147
168
return [ ]
148
169
}
149
170
// In the case of bracket-matching, we use the last character because that's
150
171
// the character that will usually cause a reformat (i.e. ` }` instead of `{`).
151
172
const character = event . newText [ event . newText . length - 1 ]
152
173
153
- const providers = [ ...this . _onTypeProviders . getAllProvidersForEditor ( editor ) ]
154
- if ( providers . length === 0 ) {
155
- return [ ]
156
- }
157
-
158
174
const contents = editor . getText ( )
159
175
const cursorPosition = editor . getCursorBufferPosition ( ) . copy ( )
160
176
0 commit comments