Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
xxadev
vscode
提交
d8e7eb36
V
vscode
项目概览
xxadev
/
vscode
与 Fork 源项目一致
从无法访问的项目Fork
通知
2
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
V
vscode
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
d8e7eb36
编写于
2月 17, 2020
作者:
B
Benjamin Pasero
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Text save participants are overwritten for each extension host (fixes #90359)
上级
6ec6f9e3
变更
12
隐藏空白更改
内联
并排
Showing
12 changed file
with
535 addition
and
388 deletion
+535
-388
extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts
...vscode-api-tests/src/singlefolder-tests/workspace.test.ts
+0
-7
src/vs/workbench/api/browser/mainThreadSaveParticipant.ts
src/vs/workbench/api/browser/mainThreadSaveParticipant.ts
+9
-340
src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts
...nch/contrib/codeEditor/browser/codeEditor.contribution.ts
+1
-0
src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts
.../workbench/contrib/codeEditor/browser/saveParticipants.ts
+316
-0
src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts
...h/contrib/codeEditor/test/browser/saveParticipant.test.ts
+2
-2
src/vs/workbench/services/textfile/browser/textFileService.ts
...vs/workbench/services/textfile/browser/textFileService.ts
+1
-3
src/vs/workbench/services/textfile/common/textFileEditorModel.ts
...workbench/services/textfile/common/textFileEditorModel.ts
+17
-5
src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts
...ch/services/textfile/common/textFileEditorModelManager.ts
+18
-1
src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts
...bench/services/textfile/common/textFileSaveParticipant.ts
+69
-0
src/vs/workbench/services/textfile/common/textfiles.ts
src/vs/workbench/services/textfile/common/textfiles.ts
+18
-13
src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts
...ervices/textfile/test/browser/textFileEditorModel.test.ts
+69
-17
src/vs/workbench/test/browser/workbenchTestServices.ts
src/vs/workbench/test/browser/workbenchTestServices.ts
+15
-0
未找到文件。
extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts
浏览文件 @
d8e7eb36
...
...
@@ -215,13 +215,6 @@ suite('workspace-namespace', () => {
});
test
(
'
eol, change via onWillSave
'
,
async
function
()
{
if
(
vscode
.
env
.
uiKind
===
vscode
.
UIKind
.
Web
)
{
// TODO@Jo Test seems to fail when running in web due to
// onWillSaveTextDocument not getting called
this
.
skip
();
return
;
}
let
called
=
false
;
let
sub
=
vscode
.
workspace
.
onWillSaveTextDocument
(
e
=>
{
called
=
true
;
...
...
src/vs/workbench/api/browser/mainThreadSaveParticipant.ts
浏览文件 @
d8e7eb36
...
...
@@ -3,304 +3,19 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import
{
IdleValue
,
raceCancellation
}
from
'
vs/base/common/async
'
;
import
{
CancellationTokenSource
,
CancellationToken
}
from
'
vs/base/common/cancellation
'
;
import
*
as
strings
from
'
vs/base/common/strings
'
;
import
{
IActiveCodeEditor
}
from
'
vs/editor/browser/editorBrowser
'
;
import
{
ICodeEditorService
}
from
'
vs/editor/browser/services/codeEditorService
'
;
import
{
trimTrailingWhitespace
}
from
'
vs/editor/common/commands/trimTrailingWhitespaceCommand
'
;
import
{
EditOperation
}
from
'
vs/editor/common/core/editOperation
'
;
import
{
Position
}
from
'
vs/editor/common/core/position
'
;
import
{
Range
}
from
'
vs/editor/common/core/range
'
;
import
{
Selection
}
from
'
vs/editor/common/core/selection
'
;
import
{
ITextModel
}
from
'
vs/editor/common/model
'
;
import
{
CodeAction
,
CodeActionTriggerType
}
from
'
vs/editor/common/modes
'
;
import
{
CancellationToken
}
from
'
vs/base/common/cancellation
'
;
import
{
shouldSynchronizeModel
}
from
'
vs/editor/common/services/modelService
'
;
import
{
getCodeActions
}
from
'
vs/editor/contrib/codeAction/codeAction
'
;
import
{
applyCodeAction
}
from
'
vs/editor/contrib/codeAction/codeActionCommands
'
;
import
{
CodeActionKind
}
from
'
vs/editor/contrib/codeAction/types
'
;
import
{
formatDocumentWithSelectedProvider
,
FormattingMode
}
from
'
vs/editor/contrib/format/format
'
;
import
{
SnippetController2
}
from
'
vs/editor/contrib/snippet/snippetController2
'
;
import
{
localize
}
from
'
vs/nls
'
;
import
{
IConfigurationService
}
from
'
vs/platform/configuration/common/configuration
'
;
import
{
IInstantiationService
}
from
'
vs/platform/instantiation/common/instantiation
'
;
import
{
ILogService
}
from
'
vs/platform/log/common/log
'
;
import
{
IProgressService
,
ProgressLocation
,
IProgressStep
,
IProgress
}
from
'
vs/platform/progress/common/progress
'
;
import
{
IProgressStep
,
IProgress
}
from
'
vs/platform/progress/common/progress
'
;
import
{
extHostCustomer
}
from
'
vs/workbench/api/common/extHostCustomers
'
;
import
{
ISaveParticipant
,
IResolvedTextFileEditorModel
,
ITextFileService
}
from
'
vs/workbench/services/textfile/common/textfiles
'
;
import
{
I
TextFile
SaveParticipant
,
IResolvedTextFileEditorModel
,
ITextFileService
}
from
'
vs/workbench/services/textfile/common/textfiles
'
;
import
{
SaveReason
}
from
'
vs/workbench/common/editor
'
;
import
{
ExtHostContext
,
ExtHostDocumentSaveParticipantShape
,
IExtHostContext
}
from
'
../common/extHost.protocol
'
;
import
{
ILabelService
}
from
'
vs/platform/label/common/label
'
;
import
{
canceled
}
from
'
vs/base/common/errors
'
;
import
{
IDisposable
}
from
'
vs/base/common/lifecycle
'
;
export
interface
ISaveParticipantParticipant
{
participate
(
model
:
IResolvedTextFileEditorModel
,
env
:
{
reason
:
SaveReason
},
progress
:
IProgress
<
IProgressStep
>
,
token
:
CancellationToken
):
Promise
<
void
>
;
}
class
TrimWhitespaceParticipant
implements
ISaveParticipantParticipant
{
constructor
(
@
IConfigurationService
private
readonly
configurationService
:
IConfigurationService
,
@
ICodeEditorService
private
readonly
codeEditorService
:
ICodeEditorService
)
{
// Nothing
}
async
participate
(
model
:
IResolvedTextFileEditorModel
,
env
:
{
reason
:
SaveReason
;
}):
Promise
<
void
>
{
if
(
this
.
configurationService
.
getValue
(
'
files.trimTrailingWhitespace
'
,
{
overrideIdentifier
:
model
.
textEditorModel
.
getLanguageIdentifier
().
language
,
resource
:
model
.
resource
}))
{
this
.
doTrimTrailingWhitespace
(
model
.
textEditorModel
,
env
.
reason
===
SaveReason
.
AUTO
);
}
}
private
doTrimTrailingWhitespace
(
model
:
ITextModel
,
isAutoSaved
:
boolean
):
void
{
let
prevSelection
:
Selection
[]
=
[];
let
cursors
:
Position
[]
=
[];
const
editor
=
findEditor
(
model
,
this
.
codeEditorService
);
if
(
editor
)
{
// Find `prevSelection` in any case do ensure a good undo stack when pushing the edit
// Collect active cursors in `cursors` only if `isAutoSaved` to avoid having the cursors jump
prevSelection
=
editor
.
getSelections
();
if
(
isAutoSaved
)
{
cursors
=
prevSelection
.
map
(
s
=>
s
.
getPosition
());
const
snippetsRange
=
SnippetController2
.
get
(
editor
).
getSessionEnclosingRange
();
if
(
snippetsRange
)
{
for
(
let
lineNumber
=
snippetsRange
.
startLineNumber
;
lineNumber
<=
snippetsRange
.
endLineNumber
;
lineNumber
++
)
{
cursors
.
push
(
new
Position
(
lineNumber
,
model
.
getLineMaxColumn
(
lineNumber
)));
}
}
}
}
const
ops
=
trimTrailingWhitespace
(
model
,
cursors
);
if
(
!
ops
.
length
)
{
return
;
// Nothing to do
}
model
.
pushEditOperations
(
prevSelection
,
ops
,
(
_edits
)
=>
prevSelection
);
}
}
function
findEditor
(
model
:
ITextModel
,
codeEditorService
:
ICodeEditorService
):
IActiveCodeEditor
|
null
{
let
candidate
:
IActiveCodeEditor
|
null
=
null
;
if
(
model
.
isAttachedToEditor
())
{
for
(
const
editor
of
codeEditorService
.
listCodeEditors
())
{
if
(
editor
.
hasModel
()
&&
editor
.
getModel
()
===
model
)
{
if
(
editor
.
hasTextFocus
())
{
return
editor
;
// favour focused editor if there are multiple
}
candidate
=
editor
;
}
}
}
return
candidate
;
}
export
class
FinalNewLineParticipant
implements
ISaveParticipantParticipant
{
constructor
(
@
IConfigurationService
private
readonly
configurationService
:
IConfigurationService
,
@
ICodeEditorService
private
readonly
codeEditorService
:
ICodeEditorService
)
{
// Nothing
}
async
participate
(
model
:
IResolvedTextFileEditorModel
,
_env
:
{
reason
:
SaveReason
;
}):
Promise
<
void
>
{
if
(
this
.
configurationService
.
getValue
(
'
files.insertFinalNewline
'
,
{
overrideIdentifier
:
model
.
textEditorModel
.
getLanguageIdentifier
().
language
,
resource
:
model
.
resource
}))
{
this
.
doInsertFinalNewLine
(
model
.
textEditorModel
);
}
}
private
doInsertFinalNewLine
(
model
:
ITextModel
):
void
{
const
lineCount
=
model
.
getLineCount
();
const
lastLine
=
model
.
getLineContent
(
lineCount
);
const
lastLineIsEmptyOrWhitespace
=
strings
.
lastNonWhitespaceIndex
(
lastLine
)
===
-
1
;
if
(
!
lineCount
||
lastLineIsEmptyOrWhitespace
)
{
return
;
}
const
edits
=
[
EditOperation
.
insert
(
new
Position
(
lineCount
,
model
.
getLineMaxColumn
(
lineCount
)),
model
.
getEOL
())];
const
editor
=
findEditor
(
model
,
this
.
codeEditorService
);
if
(
editor
)
{
editor
.
executeEdits
(
'
insertFinalNewLine
'
,
edits
,
editor
.
getSelections
());
}
else
{
model
.
pushEditOperations
([],
edits
,
()
=>
null
);
}
}
}
export
class
TrimFinalNewLinesParticipant
implements
ISaveParticipantParticipant
{
constructor
(
@
IConfigurationService
private
readonly
configurationService
:
IConfigurationService
,
@
ICodeEditorService
private
readonly
codeEditorService
:
ICodeEditorService
)
{
// Nothing
}
async
participate
(
model
:
IResolvedTextFileEditorModel
,
env
:
{
reason
:
SaveReason
;
}):
Promise
<
void
>
{
if
(
this
.
configurationService
.
getValue
(
'
files.trimFinalNewlines
'
,
{
overrideIdentifier
:
model
.
textEditorModel
.
getLanguageIdentifier
().
language
,
resource
:
model
.
resource
}))
{
this
.
doTrimFinalNewLines
(
model
.
textEditorModel
,
env
.
reason
===
SaveReason
.
AUTO
);
}
}
/**
* returns 0 if the entire file is empty or whitespace only
*/
private
findLastLineWithContent
(
model
:
ITextModel
):
number
{
for
(
let
lineNumber
=
model
.
getLineCount
();
lineNumber
>=
1
;
lineNumber
--
)
{
const
lineContent
=
model
.
getLineContent
(
lineNumber
);
if
(
strings
.
lastNonWhitespaceIndex
(
lineContent
)
!==
-
1
)
{
// this line has content
return
lineNumber
;
}
}
// no line has content
return
0
;
}
private
doTrimFinalNewLines
(
model
:
ITextModel
,
isAutoSaved
:
boolean
):
void
{
const
lineCount
=
model
.
getLineCount
();
// Do not insert new line if file does not end with new line
if
(
lineCount
===
1
)
{
return
;
}
let
prevSelection
:
Selection
[]
=
[];
let
cannotTouchLineNumber
=
0
;
const
editor
=
findEditor
(
model
,
this
.
codeEditorService
);
if
(
editor
)
{
prevSelection
=
editor
.
getSelections
();
if
(
isAutoSaved
)
{
for
(
let
i
=
0
,
len
=
prevSelection
.
length
;
i
<
len
;
i
++
)
{
const
positionLineNumber
=
prevSelection
[
i
].
positionLineNumber
;
if
(
positionLineNumber
>
cannotTouchLineNumber
)
{
cannotTouchLineNumber
=
positionLineNumber
;
}
}
}
}
const
lastLineNumberWithContent
=
this
.
findLastLineWithContent
(
model
);
const
deleteFromLineNumber
=
Math
.
max
(
lastLineNumberWithContent
+
1
,
cannotTouchLineNumber
+
1
);
const
deletionRange
=
model
.
validateRange
(
new
Range
(
deleteFromLineNumber
,
1
,
lineCount
,
model
.
getLineMaxColumn
(
lineCount
)));
if
(
deletionRange
.
isEmpty
())
{
return
;
}
model
.
pushEditOperations
(
prevSelection
,
[
EditOperation
.
delete
(
deletionRange
)],
_edits
=>
prevSelection
);
if
(
editor
)
{
editor
.
setSelections
(
prevSelection
);
}
}
}
class
FormatOnSaveParticipant
implements
ISaveParticipantParticipant
{
constructor
(
@
IConfigurationService
private
readonly
_configurationService
:
IConfigurationService
,
@
ICodeEditorService
private
readonly
_codeEditorService
:
ICodeEditorService
,
@
IInstantiationService
private
readonly
_instantiationService
:
IInstantiationService
,
)
{
// Nothing
}
async
participate
(
editorModel
:
IResolvedTextFileEditorModel
,
env
:
{
reason
:
SaveReason
;
},
progress
:
IProgress
<
IProgressStep
>
,
token
:
CancellationToken
):
Promise
<
void
>
{
const
model
=
editorModel
.
textEditorModel
;
const
overrides
=
{
overrideIdentifier
:
model
.
getLanguageIdentifier
().
language
,
resource
:
model
.
uri
};
if
(
env
.
reason
===
SaveReason
.
AUTO
||
!
this
.
_configurationService
.
getValue
(
'
editor.formatOnSave
'
,
overrides
))
{
return
undefined
;
}
progress
.
report
({
message
:
localize
(
'
formatting
'
,
"
Formatting
"
)
});
const
editorOrModel
=
findEditor
(
model
,
this
.
_codeEditorService
)
||
model
;
await
this
.
_instantiationService
.
invokeFunction
(
formatDocumentWithSelectedProvider
,
editorOrModel
,
FormattingMode
.
Silent
,
token
);
}
}
class
CodeActionOnSaveParticipant
implements
ISaveParticipantParticipant
{
constructor
(
@
IConfigurationService
private
readonly
_configurationService
:
IConfigurationService
,
@
IInstantiationService
private
readonly
_instantiationService
:
IInstantiationService
,
)
{
}
async
participate
(
editorModel
:
IResolvedTextFileEditorModel
,
env
:
{
reason
:
SaveReason
;
},
progress
:
IProgress
<
IProgressStep
>
,
token
:
CancellationToken
):
Promise
<
void
>
{
if
(
env
.
reason
===
SaveReason
.
AUTO
)
{
return
undefined
;
}
const
model
=
editorModel
.
textEditorModel
;
const
settingsOverrides
=
{
overrideIdentifier
:
model
.
getLanguageIdentifier
().
language
,
resource
:
editorModel
.
resource
};
const
setting
=
this
.
_configurationService
.
getValue
<
{
[
kind
:
string
]:
boolean
}
>
(
'
editor.codeActionsOnSave
'
,
settingsOverrides
);
if
(
!
setting
)
{
return
undefined
;
}
const
codeActionsOnSave
=
Object
.
keys
(
setting
)
.
filter
(
x
=>
setting
[
x
]).
map
(
x
=>
new
CodeActionKind
(
x
))
.
sort
((
a
,
b
)
=>
{
if
(
CodeActionKind
.
SourceFixAll
.
contains
(
a
))
{
if
(
CodeActionKind
.
SourceFixAll
.
contains
(
b
))
{
return
0
;
}
return
-
1
;
}
if
(
CodeActionKind
.
SourceFixAll
.
contains
(
b
))
{
return
1
;
}
return
0
;
});
if
(
!
codeActionsOnSave
.
length
)
{
return
undefined
;
}
const
excludedActions
=
Object
.
keys
(
setting
)
.
filter
(
x
=>
setting
[
x
]
===
false
)
.
map
(
x
=>
new
CodeActionKind
(
x
));
progress
.
report
({
message
:
localize
(
'
codeaction
'
,
"
Quick Fixes
"
)
});
await
this
.
applyOnSaveActions
(
model
,
codeActionsOnSave
,
excludedActions
,
token
);
}
private
async
applyOnSaveActions
(
model
:
ITextModel
,
codeActionsOnSave
:
readonly
CodeActionKind
[],
excludes
:
readonly
CodeActionKind
[],
token
:
CancellationToken
):
Promise
<
void
>
{
for
(
const
codeActionKind
of
codeActionsOnSave
)
{
const
actionsToRun
=
await
this
.
getActionsToRun
(
model
,
codeActionKind
,
excludes
,
token
);
try
{
await
this
.
applyCodeActions
(
actionsToRun
.
validActions
);
}
catch
{
// Failure to apply a code action should not block other on save actions
}
finally
{
actionsToRun
.
dispose
();
}
}
}
private
async
applyCodeActions
(
actionsToRun
:
readonly
CodeAction
[])
{
for
(
const
action
of
actionsToRun
)
{
await
this
.
_instantiationService
.
invokeFunction
(
applyCodeAction
,
action
);
}
}
private
getActionsToRun
(
model
:
ITextModel
,
codeActionKind
:
CodeActionKind
,
excludes
:
readonly
CodeActionKind
[],
token
:
CancellationToken
)
{
return
getCodeActions
(
model
,
model
.
getFullModelRange
(),
{
type
:
CodeActionTriggerType
.
Auto
,
filter
:
{
include
:
codeActionKind
,
excludes
:
excludes
,
includeSourceActions
:
true
},
},
token
);
}
}
class
ExtHostSaveParticipant
implements
ISaveParticipantParticipant
{
class
ExtHostSaveParticipant
implements
ITextFileSaveParticipant
{
private
readonly
_proxy
:
ExtHostDocumentSaveParticipantShape
;
...
...
@@ -336,65 +51,19 @@ class ExtHostSaveParticipant implements ISaveParticipantParticipant {
// The save participant can change a model before its saved to support various scenarios like trimming trailing whitespace
@
extHostCustomer
export
class
SaveParticipant
implements
ISaveParticipant
{
export
class
SaveParticipant
{
private
readonly
_saveParticipants
:
IdleValue
<
ISaveParticipantParticipant
[]
>
;
private
_saveParticipantDisposable
:
IDisposable
;
constructor
(
extHostContext
:
IExtHostContext
,
@
IInstantiationService
instantiationService
:
IInstantiationService
,
@
IProgressService
private
readonly
_progressService
:
IProgressService
,
@
ILogService
private
readonly
_logService
:
ILogService
,
@
ILabelService
private
readonly
_labelService
:
ILabelService
,
@
ITextFileService
private
readonly
_textFileService
:
ITextFileService
)
{
this
.
_saveParticipants
=
new
IdleValue
(()
=>
[
instantiationService
.
createInstance
(
TrimWhitespaceParticipant
),
instantiationService
.
createInstance
(
CodeActionOnSaveParticipant
),
instantiationService
.
createInstance
(
FormatOnSaveParticipant
),
instantiationService
.
createInstance
(
FinalNewLineParticipant
),
instantiationService
.
createInstance
(
TrimFinalNewLinesParticipant
),
instantiationService
.
createInstance
(
ExtHostSaveParticipant
,
extHostContext
),
]);
// Set as save participant for all text files
this
.
_textFileService
.
saveParticipant
=
this
;
this
.
_saveParticipantDisposable
=
this
.
_textFileService
.
files
.
addSaveParticipant
(
instantiationService
.
createInstance
(
ExtHostSaveParticipant
,
extHostContext
));
}
dispose
():
void
{
this
.
_textFileService
.
saveParticipant
=
undefined
;
this
.
_saveParticipants
.
dispose
();
}
async
participate
(
model
:
IResolvedTextFileEditorModel
,
context
:
{
reason
:
SaveReason
;
},
token
:
CancellationToken
):
Promise
<
void
>
{
const
cts
=
new
CancellationTokenSource
(
token
);
return
this
.
_progressService
.
withProgress
({
title
:
localize
(
'
saveParticipants
'
,
"
Running Save Participants for '{0}'
"
,
this
.
_labelService
.
getUriLabel
(
model
.
resource
,
{
relative
:
true
})),
location
:
ProgressLocation
.
Notification
,
cancellable
:
true
,
delay
:
model
.
isDirty
()
?
3000
:
5000
},
async
progress
=>
{
// undoStop before participation
model
.
textEditorModel
.
pushStackElement
();
for
(
let
p
of
this
.
_saveParticipants
.
getValue
())
{
if
(
cts
.
token
.
isCancellationRequested
)
{
break
;
}
try
{
const
promise
=
p
.
participate
(
model
,
context
,
progress
,
cts
.
token
);
await
raceCancellation
(
promise
,
cts
.
token
);
}
catch
(
err
)
{
this
.
_logService
.
warn
(
err
);
}
}
// undoStop after participation
model
.
textEditorModel
.
pushStackElement
();
},
()
=>
{
// user cancel
cts
.
dispose
(
true
);
});
this
.
_saveParticipantDisposable
.
dispose
();
}
}
src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts
浏览文件 @
d8e7eb36
...
...
@@ -9,6 +9,7 @@ import './diffEditorHelper';
import
'
./inspectKeybindings
'
;
import
'
./largeFileOptimizations
'
;
import
'
./inspectEditorTokens/inspectEditorTokens
'
;
import
'
./saveParticipants
'
;
import
'
./toggleMinimap
'
;
import
'
./toggleMultiCursorModifier
'
;
import
'
./toggleRenderControlCharacter
'
;
...
...
src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts
0 → 100644
浏览文件 @
d8e7eb36
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import
{
CancellationToken
}
from
'
vs/base/common/cancellation
'
;
import
*
as
strings
from
'
vs/base/common/strings
'
;
import
{
IActiveCodeEditor
}
from
'
vs/editor/browser/editorBrowser
'
;
import
{
ICodeEditorService
}
from
'
vs/editor/browser/services/codeEditorService
'
;
import
{
trimTrailingWhitespace
}
from
'
vs/editor/common/commands/trimTrailingWhitespaceCommand
'
;
import
{
EditOperation
}
from
'
vs/editor/common/core/editOperation
'
;
import
{
Position
}
from
'
vs/editor/common/core/position
'
;
import
{
Range
}
from
'
vs/editor/common/core/range
'
;
import
{
Selection
}
from
'
vs/editor/common/core/selection
'
;
import
{
ITextModel
}
from
'
vs/editor/common/model
'
;
import
{
CodeAction
,
CodeActionTriggerType
}
from
'
vs/editor/common/modes
'
;
import
{
getCodeActions
}
from
'
vs/editor/contrib/codeAction/codeAction
'
;
import
{
applyCodeAction
}
from
'
vs/editor/contrib/codeAction/codeActionCommands
'
;
import
{
CodeActionKind
}
from
'
vs/editor/contrib/codeAction/types
'
;
import
{
formatDocumentWithSelectedProvider
,
FormattingMode
}
from
'
vs/editor/contrib/format/format
'
;
import
{
SnippetController2
}
from
'
vs/editor/contrib/snippet/snippetController2
'
;
import
{
localize
}
from
'
vs/nls
'
;
import
{
IConfigurationService
}
from
'
vs/platform/configuration/common/configuration
'
;
import
{
IInstantiationService
}
from
'
vs/platform/instantiation/common/instantiation
'
;
import
{
IProgressStep
,
IProgress
}
from
'
vs/platform/progress/common/progress
'
;
import
{
IResolvedTextFileEditorModel
,
ITextFileService
,
ITextFileSaveParticipant
}
from
'
vs/workbench/services/textfile/common/textfiles
'
;
import
{
SaveReason
}
from
'
vs/workbench/common/editor
'
;
import
{
Disposable
}
from
'
vs/base/common/lifecycle
'
;
import
{
IWorkbenchContribution
,
Extensions
as
WorkbenchContributionsExtensions
,
IWorkbenchContributionsRegistry
}
from
'
vs/workbench/common/contributions
'
;
import
{
Registry
}
from
'
vs/platform/registry/common/platform
'
;
import
{
LifecyclePhase
}
from
'
vs/platform/lifecycle/common/lifecycle
'
;
class
TrimWhitespaceParticipant
implements
ITextFileSaveParticipant
{
constructor
(
@
IConfigurationService
private
readonly
configurationService
:
IConfigurationService
,
@
ICodeEditorService
private
readonly
codeEditorService
:
ICodeEditorService
)
{
// Nothing
}
async
participate
(
model
:
IResolvedTextFileEditorModel
,
env
:
{
reason
:
SaveReason
;
}):
Promise
<
void
>
{
if
(
this
.
configurationService
.
getValue
(
'
files.trimTrailingWhitespace
'
,
{
overrideIdentifier
:
model
.
textEditorModel
.
getLanguageIdentifier
().
language
,
resource
:
model
.
resource
}))
{
this
.
doTrimTrailingWhitespace
(
model
.
textEditorModel
,
env
.
reason
===
SaveReason
.
AUTO
);
}
}
private
doTrimTrailingWhitespace
(
model
:
ITextModel
,
isAutoSaved
:
boolean
):
void
{
let
prevSelection
:
Selection
[]
=
[];
let
cursors
:
Position
[]
=
[];
const
editor
=
findEditor
(
model
,
this
.
codeEditorService
);
if
(
editor
)
{
// Find `prevSelection` in any case do ensure a good undo stack when pushing the edit
// Collect active cursors in `cursors` only if `isAutoSaved` to avoid having the cursors jump
prevSelection
=
editor
.
getSelections
();
if
(
isAutoSaved
)
{
cursors
=
prevSelection
.
map
(
s
=>
s
.
getPosition
());
const
snippetsRange
=
SnippetController2
.
get
(
editor
).
getSessionEnclosingRange
();
if
(
snippetsRange
)
{
for
(
let
lineNumber
=
snippetsRange
.
startLineNumber
;
lineNumber
<=
snippetsRange
.
endLineNumber
;
lineNumber
++
)
{
cursors
.
push
(
new
Position
(
lineNumber
,
model
.
getLineMaxColumn
(
lineNumber
)));
}
}
}
}
const
ops
=
trimTrailingWhitespace
(
model
,
cursors
);
if
(
!
ops
.
length
)
{
return
;
// Nothing to do
}
model
.
pushEditOperations
(
prevSelection
,
ops
,
(
_edits
)
=>
prevSelection
);
}
}
function
findEditor
(
model
:
ITextModel
,
codeEditorService
:
ICodeEditorService
):
IActiveCodeEditor
|
null
{
let
candidate
:
IActiveCodeEditor
|
null
=
null
;
if
(
model
.
isAttachedToEditor
())
{
for
(
const
editor
of
codeEditorService
.
listCodeEditors
())
{
if
(
editor
.
hasModel
()
&&
editor
.
getModel
()
===
model
)
{
if
(
editor
.
hasTextFocus
())
{
return
editor
;
// favour focused editor if there are multiple
}
candidate
=
editor
;
}
}
}
return
candidate
;
}
export
class
FinalNewLineParticipant
implements
ITextFileSaveParticipant
{
constructor
(
@
IConfigurationService
private
readonly
configurationService
:
IConfigurationService
,
@
ICodeEditorService
private
readonly
codeEditorService
:
ICodeEditorService
)
{
// Nothing
}
async
participate
(
model
:
IResolvedTextFileEditorModel
,
_env
:
{
reason
:
SaveReason
;
}):
Promise
<
void
>
{
if
(
this
.
configurationService
.
getValue
(
'
files.insertFinalNewline
'
,
{
overrideIdentifier
:
model
.
textEditorModel
.
getLanguageIdentifier
().
language
,
resource
:
model
.
resource
}))
{
this
.
doInsertFinalNewLine
(
model
.
textEditorModel
);
}
}
private
doInsertFinalNewLine
(
model
:
ITextModel
):
void
{
const
lineCount
=
model
.
getLineCount
();
const
lastLine
=
model
.
getLineContent
(
lineCount
);
const
lastLineIsEmptyOrWhitespace
=
strings
.
lastNonWhitespaceIndex
(
lastLine
)
===
-
1
;
if
(
!
lineCount
||
lastLineIsEmptyOrWhitespace
)
{
return
;
}
const
edits
=
[
EditOperation
.
insert
(
new
Position
(
lineCount
,
model
.
getLineMaxColumn
(
lineCount
)),
model
.
getEOL
())];
const
editor
=
findEditor
(
model
,
this
.
codeEditorService
);
if
(
editor
)
{
editor
.
executeEdits
(
'
insertFinalNewLine
'
,
edits
,
editor
.
getSelections
());
}
else
{
model
.
pushEditOperations
([],
edits
,
()
=>
null
);
}
}
}
export
class
TrimFinalNewLinesParticipant
implements
ITextFileSaveParticipant
{
constructor
(
@
IConfigurationService
private
readonly
configurationService
:
IConfigurationService
,
@
ICodeEditorService
private
readonly
codeEditorService
:
ICodeEditorService
)
{
// Nothing
}
async
participate
(
model
:
IResolvedTextFileEditorModel
,
env
:
{
reason
:
SaveReason
;
}):
Promise
<
void
>
{
if
(
this
.
configurationService
.
getValue
(
'
files.trimFinalNewlines
'
,
{
overrideIdentifier
:
model
.
textEditorModel
.
getLanguageIdentifier
().
language
,
resource
:
model
.
resource
}))
{
this
.
doTrimFinalNewLines
(
model
.
textEditorModel
,
env
.
reason
===
SaveReason
.
AUTO
);
}
}
/**
* returns 0 if the entire file is empty or whitespace only
*/
private
findLastLineWithContent
(
model
:
ITextModel
):
number
{
for
(
let
lineNumber
=
model
.
getLineCount
();
lineNumber
>=
1
;
lineNumber
--
)
{
const
lineContent
=
model
.
getLineContent
(
lineNumber
);
if
(
strings
.
lastNonWhitespaceIndex
(
lineContent
)
!==
-
1
)
{
// this line has content
return
lineNumber
;
}
}
// no line has content
return
0
;
}
private
doTrimFinalNewLines
(
model
:
ITextModel
,
isAutoSaved
:
boolean
):
void
{
const
lineCount
=
model
.
getLineCount
();
// Do not insert new line if file does not end with new line
if
(
lineCount
===
1
)
{
return
;
}
let
prevSelection
:
Selection
[]
=
[];
let
cannotTouchLineNumber
=
0
;
const
editor
=
findEditor
(
model
,
this
.
codeEditorService
);
if
(
editor
)
{
prevSelection
=
editor
.
getSelections
();
if
(
isAutoSaved
)
{
for
(
let
i
=
0
,
len
=
prevSelection
.
length
;
i
<
len
;
i
++
)
{
const
positionLineNumber
=
prevSelection
[
i
].
positionLineNumber
;
if
(
positionLineNumber
>
cannotTouchLineNumber
)
{
cannotTouchLineNumber
=
positionLineNumber
;
}
}
}
}
const
lastLineNumberWithContent
=
this
.
findLastLineWithContent
(
model
);
const
deleteFromLineNumber
=
Math
.
max
(
lastLineNumberWithContent
+
1
,
cannotTouchLineNumber
+
1
);
const
deletionRange
=
model
.
validateRange
(
new
Range
(
deleteFromLineNumber
,
1
,
lineCount
,
model
.
getLineMaxColumn
(
lineCount
)));
if
(
deletionRange
.
isEmpty
())
{
return
;
}
model
.
pushEditOperations
(
prevSelection
,
[
EditOperation
.
delete
(
deletionRange
)],
_edits
=>
prevSelection
);
if
(
editor
)
{
editor
.
setSelections
(
prevSelection
);
}
}
}
class
FormatOnSaveParticipant
implements
ITextFileSaveParticipant
{
constructor
(
@
IConfigurationService
private
readonly
configurationService
:
IConfigurationService
,
@
ICodeEditorService
private
readonly
codeEditorService
:
ICodeEditorService
,
@
IInstantiationService
private
readonly
instantiationService
:
IInstantiationService
,
)
{
// Nothing
}
async
participate
(
editorModel
:
IResolvedTextFileEditorModel
,
env
:
{
reason
:
SaveReason
;
},
progress
:
IProgress
<
IProgressStep
>
,
token
:
CancellationToken
):
Promise
<
void
>
{
const
model
=
editorModel
.
textEditorModel
;
const
overrides
=
{
overrideIdentifier
:
model
.
getLanguageIdentifier
().
language
,
resource
:
model
.
uri
};
if
(
env
.
reason
===
SaveReason
.
AUTO
||
!
this
.
configurationService
.
getValue
(
'
editor.formatOnSave
'
,
overrides
))
{
return
undefined
;
}
progress
.
report
({
message
:
localize
(
'
formatting
'
,
"
Formatting
"
)
});
const
editorOrModel
=
findEditor
(
model
,
this
.
codeEditorService
)
||
model
;
await
this
.
instantiationService
.
invokeFunction
(
formatDocumentWithSelectedProvider
,
editorOrModel
,
FormattingMode
.
Silent
,
token
);
}
}
class
CodeActionOnSaveParticipant
implements
ITextFileSaveParticipant
{
constructor
(
@
IConfigurationService
private
readonly
configurationService
:
IConfigurationService
,
@
IInstantiationService
private
readonly
instantiationService
:
IInstantiationService
,
)
{
}
async
participate
(
editorModel
:
IResolvedTextFileEditorModel
,
env
:
{
reason
:
SaveReason
;
},
progress
:
IProgress
<
IProgressStep
>
,
token
:
CancellationToken
):
Promise
<
void
>
{
if
(
env
.
reason
===
SaveReason
.
AUTO
)
{
return
undefined
;
}
const
model
=
editorModel
.
textEditorModel
;
const
settingsOverrides
=
{
overrideIdentifier
:
model
.
getLanguageIdentifier
().
language
,
resource
:
editorModel
.
resource
};
const
setting
=
this
.
configurationService
.
getValue
<
{
[
kind
:
string
]:
boolean
}
>
(
'
editor.codeActionsOnSave
'
,
settingsOverrides
);
if
(
!
setting
)
{
return
undefined
;
}
const
codeActionsOnSave
=
Object
.
keys
(
setting
)
.
filter
(
x
=>
setting
[
x
]).
map
(
x
=>
new
CodeActionKind
(
x
))
.
sort
((
a
,
b
)
=>
{
if
(
CodeActionKind
.
SourceFixAll
.
contains
(
a
))
{
if
(
CodeActionKind
.
SourceFixAll
.
contains
(
b
))
{
return
0
;
}
return
-
1
;
}
if
(
CodeActionKind
.
SourceFixAll
.
contains
(
b
))
{
return
1
;
}
return
0
;
});
if
(
!
codeActionsOnSave
.
length
)
{
return
undefined
;
}
const
excludedActions
=
Object
.
keys
(
setting
)
.
filter
(
x
=>
setting
[
x
]
===
false
)
.
map
(
x
=>
new
CodeActionKind
(
x
));
progress
.
report
({
message
:
localize
(
'
codeaction
'
,
"
Quick Fixes
"
)
});
await
this
.
applyOnSaveActions
(
model
,
codeActionsOnSave
,
excludedActions
,
token
);
}
private
async
applyOnSaveActions
(
model
:
ITextModel
,
codeActionsOnSave
:
readonly
CodeActionKind
[],
excludes
:
readonly
CodeActionKind
[],
token
:
CancellationToken
):
Promise
<
void
>
{
for
(
const
codeActionKind
of
codeActionsOnSave
)
{
const
actionsToRun
=
await
this
.
getActionsToRun
(
model
,
codeActionKind
,
excludes
,
token
);
try
{
await
this
.
applyCodeActions
(
actionsToRun
.
validActions
);
}
catch
{
// Failure to apply a code action should not block other on save actions
}
finally
{
actionsToRun
.
dispose
();
}
}
}
private
async
applyCodeActions
(
actionsToRun
:
readonly
CodeAction
[])
{
for
(
const
action
of
actionsToRun
)
{
await
this
.
instantiationService
.
invokeFunction
(
applyCodeAction
,
action
);
}
}
private
getActionsToRun
(
model
:
ITextModel
,
codeActionKind
:
CodeActionKind
,
excludes
:
readonly
CodeActionKind
[],
token
:
CancellationToken
)
{
return
getCodeActions
(
model
,
model
.
getFullModelRange
(),
{
type
:
CodeActionTriggerType
.
Auto
,
filter
:
{
include
:
codeActionKind
,
excludes
:
excludes
,
includeSourceActions
:
true
},
},
token
);
}
}
export
class
SaveParticipantsContribution
extends
Disposable
implements
IWorkbenchContribution
{
constructor
(
@
IInstantiationService
private
readonly
instantiationService
:
IInstantiationService
,
@
ITextFileService
private
readonly
textFileService
:
ITextFileService
)
{
super
();
this
.
registerSaveParticipants
();
}
private
registerSaveParticipants
():
void
{
this
.
_register
(
this
.
textFileService
.
files
.
addSaveParticipant
(
this
.
instantiationService
.
createInstance
(
TrimWhitespaceParticipant
)));
this
.
_register
(
this
.
textFileService
.
files
.
addSaveParticipant
(
this
.
instantiationService
.
createInstance
(
CodeActionOnSaveParticipant
)));
this
.
_register
(
this
.
textFileService
.
files
.
addSaveParticipant
(
this
.
instantiationService
.
createInstance
(
FormatOnSaveParticipant
)));
this
.
_register
(
this
.
textFileService
.
files
.
addSaveParticipant
(
this
.
instantiationService
.
createInstance
(
FinalNewLineParticipant
)));
this
.
_register
(
this
.
textFileService
.
files
.
addSaveParticipant
(
this
.
instantiationService
.
createInstance
(
TrimFinalNewLinesParticipant
)));
}
}
const
workbenchContributionsRegistry
=
Registry
.
as
<
IWorkbenchContributionsRegistry
>
(
WorkbenchContributionsExtensions
.
Workbench
);
workbenchContributionsRegistry
.
registerWorkbenchContribution
(
SaveParticipantsContribution
,
LifecyclePhase
.
Restored
);
src/vs/workbench/
test/electron-browser/api/mainThreadS
aveParticipant.test.ts
→
src/vs/workbench/
contrib/codeEditor/test/browser/s
aveParticipant.test.ts
浏览文件 @
d8e7eb36
...
...
@@ -5,9 +5,9 @@
import
*
as
assert
from
'
assert
'
;
import
{
IInstantiationService
}
from
'
vs/platform/instantiation/common/instantiation
'
;
import
{
FinalNewLineParticipant
,
TrimFinalNewLinesParticipant
}
from
'
vs/workbench/
api/browser/mainThreadSaveParticipant
'
;
import
{
FinalNewLineParticipant
,
TrimFinalNewLinesParticipant
}
from
'
vs/workbench/
contrib/codeEditor/browser/saveParticipants
'
;
import
{
TestConfigurationService
}
from
'
vs/platform/configuration/test/common/testConfigurationService
'
;
import
{
workbenchInstantiationService
,
TestTextFileService
}
from
'
vs/workbench/test/
electron-
browser/workbenchTestServices
'
;
import
{
workbenchInstantiationService
,
TestTextFileService
}
from
'
vs/workbench/test/browser/workbenchTestServices
'
;
import
{
toResource
}
from
'
vs/base/test/common/utils
'
;
import
{
IModelService
}
from
'
vs/editor/common/services/modelService
'
;
import
{
Range
}
from
'
vs/editor/common/core/range
'
;
...
...
src/vs/workbench/services/textfile/browser/textFileService.ts
浏览文件 @
d8e7eb36
...
...
@@ -6,7 +6,7 @@
import
*
as
nls
from
'
vs/nls
'
;
import
{
URI
}
from
'
vs/base/common/uri
'
;
import
{
Emitter
,
AsyncEmitter
}
from
'
vs/base/common/event
'
;
import
{
IResult
,
ITextFileOperationResult
,
ITextFileService
,
ITextFileStreamContent
,
ITextFileEditorModel
,
ITextFileContent
,
IResourceEncodings
,
IReadTextFileOptions
,
IWriteTextFileOptions
,
toBufferOrReadable
,
TextFileOperationError
,
TextFileOperationResult
,
FileOperationWillRunEvent
,
FileOperationDidRunEvent
,
ITextFileSaveOptions
,
ITextFileEditorModelManager
,
ISaveParticipant
}
from
'
vs/workbench/services/textfile/common/textfiles
'
;
import
{
IResult
,
ITextFileOperationResult
,
ITextFileService
,
ITextFileStreamContent
,
ITextFileEditorModel
,
ITextFileContent
,
IResourceEncodings
,
IReadTextFileOptions
,
IWriteTextFileOptions
,
toBufferOrReadable
,
TextFileOperationError
,
TextFileOperationResult
,
FileOperationWillRunEvent
,
FileOperationDidRunEvent
,
ITextFileSaveOptions
,
ITextFileEditorModelManager
}
from
'
vs/workbench/services/textfile/common/textfiles
'
;
import
{
IRevertOptions
,
IEncodingSupport
}
from
'
vs/workbench/common/editor
'
;
import
{
ILifecycleService
}
from
'
vs/platform/lifecycle/common/lifecycle
'
;
import
{
IFileService
,
FileOperationError
,
FileOperationResult
,
IFileStatWithMetadata
,
ICreateFileOptions
,
FileOperation
}
from
'
vs/platform/files/common/files
'
;
...
...
@@ -69,8 +69,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
};
})();
saveParticipant
:
ISaveParticipant
|
undefined
=
undefined
;
abstract
get
encoding
():
IResourceEncodings
;
constructor
(
...
...
src/vs/workbench/services/textfile/common/textFileEditorModel.ts
浏览文件 @
d8e7eb36
...
...
@@ -616,7 +616,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this
.
textEditorModel
.
pushStackElement
();
}
const
save
Participant
Cancellation
=
new
CancellationTokenSource
();
const
saveCancellation
=
new
CancellationTokenSource
();
return
this
.
saveSequentializer
.
setPending
(
versionId
,
(
async
()
=>
{
...
...
@@ -625,14 +625,26 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// In addition we update our version right after in case it changed because of a model change
//
// Save participants can also be skipped through API.
if
(
this
.
isResolved
()
&&
this
.
textFileService
.
saveParticipant
&&
!
options
.
skipSaveParticipants
)
{
if
(
this
.
isResolved
()
&&
!
options
.
skipSaveParticipants
)
{
try
{
await
this
.
textFileService
.
saveParticipant
.
participate
(
this
,
{
reason
:
options
.
reason
??
SaveReason
.
EXPLICIT
},
saveParticipant
Cancellation
.
token
);
await
this
.
textFileService
.
files
.
runSaveParticipants
(
this
,
{
reason
:
options
.
reason
??
SaveReason
.
EXPLICIT
},
save
Cancellation
.
token
);
}
catch
(
error
)
{
// Ignore
this
.
logService
.
error
(
`[text file model] runSaveParticipants(
${
versionId
}
) - resulted in an error:
${
error
.
toString
()}
`
,
this
.
resource
.
toString
());
}
}
// It is possible that a subsequent save is cancelling this
// running save. As such we return early when we detect that
// However, we do not pass the token into the file service
// because that is an atomic operation currently without
// cancellation support, so we dispose the cancellation if
// it was not cancelled yet.
if
(
saveCancellation
.
token
.
isCancellationRequested
)
{
return
;
}
else
{
saveCancellation
.
dispose
();
}
// We have to protect against being disposed at this point. It could be that the save() operation
// was triggerd followed by a dispose() operation right after without waiting. Typically we cannot
// be disposed if we are dirty, but if we are not dirty, save() and dispose() can still be triggered
...
...
@@ -687,7 +699,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this
.
handleSaveError
(
error
,
versionId
,
options
);
}
})());
})(),
()
=>
save
Participant
Cancellation
.
cancel
());
})(),
()
=>
saveCancellation
.
cancel
());
}
private
handleSaveSuccess
(
stat
:
IFileStatWithMetadata
,
versionId
:
number
,
options
:
ITextFileSaveOptions
):
void
{
...
...
src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts
浏览文件 @
d8e7eb36
...
...
@@ -7,7 +7,7 @@ import { Emitter } from 'vs/base/common/event';
import
{
URI
}
from
'
vs/base/common/uri
'
;
import
{
TextFileEditorModel
}
from
'
vs/workbench/services/textfile/common/textFileEditorModel
'
;
import
{
dispose
,
IDisposable
,
Disposable
,
DisposableStore
}
from
'
vs/base/common/lifecycle
'
;
import
{
ITextFileEditorModel
,
ITextFileEditorModelManager
,
IModelLoadOrCreateOptions
,
ITextFileModelLoadEvent
,
ITextFileModelSaveEvent
}
from
'
vs/workbench/services/textfile/common/textfiles
'
;
import
{
ITextFileEditorModel
,
ITextFileEditorModelManager
,
IModelLoadOrCreateOptions
,
ITextFileModelLoadEvent
,
ITextFileModelSaveEvent
,
ITextFileSaveParticipant
,
IResolvedTextFileEditorModel
}
from
'
vs/workbench/services/textfile/common/textfiles
'
;
import
{
ILifecycleService
}
from
'
vs/platform/lifecycle/common/lifecycle
'
;
import
{
IInstantiationService
}
from
'
vs/platform/instantiation/common/instantiation
'
;
import
{
ResourceMap
}
from
'
vs/base/common/map
'
;
...
...
@@ -15,6 +15,9 @@ import { IFileService, FileChangesEvent } from 'vs/platform/files/common/files';
import
{
distinct
,
coalesce
}
from
'
vs/base/common/arrays
'
;
import
{
ResourceQueue
}
from
'
vs/base/common/async
'
;
import
{
onUnexpectedError
}
from
'
vs/base/common/errors
'
;
import
{
TextFileSaveParticipant
}
from
'
vs/workbench/services/textfile/common/textFileSaveParticipant
'
;
import
{
SaveReason
}
from
'
vs/workbench/common/editor
'
;
import
{
CancellationToken
}
from
'
vs/base/common/cancellation
'
;
export
class
TextFileEditorModelManager
extends
Disposable
implements
ITextFileEditorModelManager
{
...
...
@@ -227,6 +230,20 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
}
}
//#region Save participants
private
readonly
saveParticipants
=
this
.
_register
(
this
.
instantiationService
.
createInstance
(
TextFileSaveParticipant
));
addSaveParticipant
(
participant
:
ITextFileSaveParticipant
):
IDisposable
{
return
this
.
saveParticipants
.
addSaveParticipant
(
participant
);
}
runSaveParticipants
(
model
:
IResolvedTextFileEditorModel
,
context
:
{
reason
:
SaveReason
;
},
token
:
CancellationToken
):
Promise
<
void
>
{
return
this
.
saveParticipants
.
participate
(
model
,
context
,
token
);
}
//#endregion
clear
():
void
{
// model caches
...
...
src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts
0 → 100644
浏览文件 @
d8e7eb36
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import
{
raceCancellation
}
from
'
vs/base/common/async
'
;
import
{
CancellationTokenSource
,
CancellationToken
}
from
'
vs/base/common/cancellation
'
;
import
{
localize
}
from
'
vs/nls
'
;
import
{
ILogService
}
from
'
vs/platform/log/common/log
'
;
import
{
IProgressService
,
ProgressLocation
}
from
'
vs/platform/progress/common/progress
'
;
import
{
ITextFileSaveParticipant
,
IResolvedTextFileEditorModel
}
from
'
vs/workbench/services/textfile/common/textfiles
'
;
import
{
SaveReason
}
from
'
vs/workbench/common/editor
'
;
import
{
IDisposable
,
Disposable
,
toDisposable
}
from
'
vs/base/common/lifecycle
'
;
export
class
TextFileSaveParticipant
extends
Disposable
{
private
readonly
saveParticipants
:
ITextFileSaveParticipant
[]
=
[];
constructor
(
@
IProgressService
private
readonly
progressService
:
IProgressService
,
@
ILogService
private
readonly
logService
:
ILogService
)
{
super
();
}
addSaveParticipant
(
participant
:
ITextFileSaveParticipant
):
IDisposable
{
this
.
saveParticipants
.
push
(
participant
);
return
toDisposable
(()
=>
this
.
saveParticipants
.
splice
(
this
.
saveParticipants
.
indexOf
(
participant
),
1
));
}
participate
(
model
:
IResolvedTextFileEditorModel
,
context
:
{
reason
:
SaveReason
;
},
token
:
CancellationToken
):
Promise
<
void
>
{
const
cts
=
new
CancellationTokenSource
(
token
);
return
this
.
progressService
.
withProgress
({
title
:
localize
(
'
saveParticipants
'
,
"
Running Save Participants for '{0}'
"
,
model
.
name
),
location
:
ProgressLocation
.
Notification
,
cancellable
:
true
,
delay
:
model
.
isDirty
()
?
3000
:
5000
},
async
progress
=>
{
// undoStop before participation
model
.
textEditorModel
.
pushStackElement
();
for
(
const
saveParticipant
of
this
.
saveParticipants
)
{
if
(
cts
.
token
.
isCancellationRequested
)
{
break
;
}
try
{
const
promise
=
saveParticipant
.
participate
(
model
,
context
,
progress
,
cts
.
token
);
await
raceCancellation
(
promise
,
cts
.
token
);
}
catch
(
err
)
{
this
.
logService
.
warn
(
err
);
}
}
// undoStop after participation
model
.
textEditorModel
.
pushStackElement
();
},
()
=>
{
// user cancel
cts
.
dispose
(
true
);
});
}
dispose
():
void
{
this
.
saveParticipants
.
splice
(
0
,
this
.
saveParticipants
.
length
);
}
}
src/vs/workbench/services/textfile/common/textfiles.ts
浏览文件 @
d8e7eb36
...
...
@@ -17,6 +17,7 @@ import { isNative } from 'vs/base/common/platform';
import
{
IWorkingCopy
}
from
'
vs/workbench/services/workingCopy/common/workingCopyService
'
;
import
{
IUntitledTextEditorModelManager
}
from
'
vs/workbench/services/untitled/common/untitledTextEditorService
'
;
import
{
CancellationToken
}
from
'
vs/base/common/cancellation
'
;
import
{
IProgress
,
IProgressStep
}
from
'
vs/platform/progress/common/progress
'
;
export
const
ITextFileService
=
createDecorator
<
ITextFileService
>
(
'
textFileService
'
);
...
...
@@ -57,11 +58,6 @@ export interface ITextFileService extends IDisposable {
*/
saveErrorHandler
:
ISaveErrorHandler
;
/**
* The save participant if any. By default, no save participant is registered.
*/
saveParticipant
:
ISaveParticipant
|
undefined
;
/**
* A resource is dirty if it has unsaved changes or is an untitled file not yet saved.
*
...
...
@@ -226,14 +222,6 @@ export interface ISaveErrorHandler {
onSaveError
(
error
:
Error
,
model
:
ITextFileEditorModel
):
void
;
}
export
interface
ISaveParticipant
{
/**
* Participate in a save of a model. Allows to change the model before it is being saved to disk.
*/
participate
(
model
:
IResolvedTextFileEditorModel
,
context
:
{
reason
:
SaveReason
},
token
:
CancellationToken
):
Promise
<
void
>
;
}
/**
* States the text file editor model can be in.
*/
...
...
@@ -357,6 +345,20 @@ export interface ITextFileModelLoadEvent {
reason
:
LoadReason
;
}
export
interface
ITextFileSaveParticipant
{
/**
* Participate in a save of a model. Allows to change the model
* before it is being saved to disk.
*/
participate
(
model
:
IResolvedTextFileEditorModel
,
context
:
{
reason
:
SaveReason
},
progress
:
IProgress
<
IProgressStep
>
,
token
:
CancellationToken
):
Promise
<
void
>
;
}
export
interface
ITextFileEditorModelManager
{
readonly
onDidLoad
:
Event
<
ITextFileModelLoadEvent
>
;
...
...
@@ -372,6 +374,9 @@ export interface ITextFileEditorModelManager {
resolve
(
resource
:
URI
,
options
?:
IModelLoadOrCreateOptions
):
Promise
<
ITextFileEditorModel
>
;
addSaveParticipant
(
participant
:
ITextFileSaveParticipant
):
IDisposable
;
runSaveParticipants
(
model
:
IResolvedTextFileEditorModel
,
context
:
{
reason
:
SaveReason
;
},
token
:
CancellationToken
):
Promise
<
void
>
disposeModel
(
model
:
ITextFileEditorModel
):
void
;
}
...
...
src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts
浏览文件 @
d8e7eb36
...
...
@@ -502,55 +502,98 @@ suite('Files - TextFileEditorModel', () => {
eventCounter
++
;
});
accessor
.
textFileService
.
saveParticipant
=
{
const
participant
=
accessor
.
textFileService
.
files
.
addSaveParticipant
(
{
participate
:
async
model
=>
{
assert
.
ok
(
model
.
isDirty
());
model
.
textEditorModel
!
.
setValue
(
'
bar
'
);
assert
.
ok
(
model
.
isDirty
());
eventCounter
++
;
}
};
}
)
;
await
model
.
load
();
model
.
textEditorModel
!
.
setValue
(
'
foo
'
);
await
model
.
save
();
model
.
dispose
();
assert
.
equal
(
eventCounter
,
2
);
participant
.
dispose
();
model
.
textEditorModel
!
.
setValue
(
'
bar
'
);
await
model
.
save
();
assert
.
equal
(
eventCounter
,
3
);
model
.
dispose
();
});
test
(
'
Save Participant - skip
'
,
async
function
()
{
let
eventCounter
=
0
;
const
model
:
TextFileEditorModel
=
instantiationService
.
createInstance
(
TextFileEditorModel
,
toResource
.
call
(
this
,
'
/path/index_async.txt
'
),
'
utf8
'
,
undefined
);
const
participant
=
accessor
.
textFileService
.
files
.
addSaveParticipant
({
participate
:
async
model
=>
{
eventCounter
++
;
}
});
await
model
.
load
();
model
.
textEditorModel
!
.
setValue
(
'
foo
'
);
await
model
.
save
({
skipSaveParticipants
:
true
});
assert
.
equal
(
eventCounter
,
0
);
participant
.
dispose
();
model
.
dispose
();
});
test
(
'
Save Participant, async participant
'
,
async
function
()
{
let
eventCounter
=
0
;
const
model
:
TextFileEditorModel
=
instantiationService
.
createInstance
(
TextFileEditorModel
,
toResource
.
call
(
this
,
'
/path/index_async.txt
'
),
'
utf8
'
,
undefined
);
accessor
.
textFileService
.
saveParticipant
=
{
participate
:
(
model
)
=>
{
model
.
onDidSave
(
e
=>
{
assert
.
ok
(
!
model
.
isDirty
());
eventCounter
++
;
});
const
participant
=
accessor
.
textFileService
.
files
.
addSaveParticipant
({
participate
:
model
=>
{
assert
.
ok
(
model
.
isDirty
());
model
.
textEditorModel
!
.
setValue
(
'
bar
'
);
assert
.
ok
(
model
.
isDirty
());
eventCounter
++
;
return
timeout
(
10
);
}
};
}
)
;
await
model
.
load
();
model
.
textEditorModel
!
.
setValue
(
'
foo
'
);
const
now
=
Date
.
now
();
await
model
.
save
();
assert
.
equal
(
eventCounter
,
2
);
assert
.
ok
(
Date
.
now
()
-
now
>=
10
);
model
.
dispose
();
participant
.
dispose
();
});
test
(
'
Save Participant, bad participant
'
,
async
function
()
{
const
model
:
TextFileEditorModel
=
instantiationService
.
createInstance
(
TextFileEditorModel
,
toResource
.
call
(
this
,
'
/path/index_async.txt
'
),
'
utf8
'
,
undefined
);
accessor
.
textFileService
.
saveParticipant
=
{
const
participant
=
accessor
.
textFileService
.
files
.
addSaveParticipant
(
{
participate
:
async
model
=>
{
new
Error
(
'
boom
'
);
}
};
}
)
;
await
model
.
load
();
model
.
textEditorModel
!
.
setValue
(
'
foo
'
);
await
model
.
save
();
model
.
dispose
();
participant
.
dispose
();
});
test
(
'
Save Participant, participant cancelled when saved again
'
,
async
function
()
{
...
...
@@ -558,12 +601,15 @@ suite('Files - TextFileEditorModel', () => {
let
participations
:
boolean
[]
=
[];
accessor
.
textFileService
.
saveParticipant
=
{
participate
:
async
model
=>
{
const
participant
=
accessor
.
textFileService
.
files
.
addSaveParticipant
(
{
participate
:
async
(
model
,
context
,
progress
,
token
)
=>
{
await
timeout
(
10
);
participations
.
push
(
true
);
if
(
!
token
.
isCancellationRequested
)
{
participations
.
push
(
true
);
}
}
};
}
)
;
await
model
.
load
();
...
...
@@ -574,12 +620,16 @@ suite('Files - TextFileEditorModel', () => {
const
p2
=
model
.
save
();
model
.
textEditorModel
!
.
setValue
(
'
foo 2
'
);
await
model
.
save
();
const
p3
=
model
.
save
();
model
.
textEditorModel
!
.
setValue
(
'
foo 3
'
);
const
p4
=
model
.
save
();
await
p1
;
await
p2
;
await
Promise
.
all
([
p1
,
p2
,
p3
,
p4
]);
assert
.
equal
(
participations
.
length
,
1
);
model
.
dispose
();
participant
.
dispose
();
});
test
(
'
Save Participant, calling save from within is unsupported but does not explode (sync save)
'
,
async
function
()
{
...
...
@@ -602,7 +652,7 @@ suite('Files - TextFileEditorModel', () => {
let
savePromise
:
Promise
<
boolean
>
;
let
breakLoop
=
false
;
accessor
.
textFileService
.
saveParticipant
=
{
const
participant
=
accessor
.
textFileService
.
files
.
addSaveParticipant
(
{
participate
:
async
model
=>
{
if
(
breakLoop
)
{
return
;
...
...
@@ -618,12 +668,14 @@ suite('Files - TextFileEditorModel', () => {
// assert that this is the same promise as the outer one
assert
.
equal
(
savePromise
,
newSavePromise
);
}
};
}
)
;
await
model
.
load
();
model
.
textEditorModel
!
.
setValue
(
'
foo
'
);
savePromise
=
model
.
save
();
await
savePromise
;
participant
.
dispose
();
}
});
src/vs/workbench/test/browser/workbenchTestServices.ts
浏览文件 @
d8e7eb36
...
...
@@ -89,6 +89,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
import
{
createTextBufferFactoryFromStream
}
from
'
vs/editor/common/model/textModel
'
;
import
{
IRemotePathService
}
from
'
vs/workbench/services/path/common/remotePathService
'
;
import
{
Direction
}
from
'
vs/base/browser/ui/grid/grid
'
;
import
{
IProgressService
,
IProgressOptions
,
IProgressWindowOptions
,
IProgressNotificationOptions
,
IProgressCompositeOptions
,
IProgress
,
IProgressStep
,
emptyProgress
}
from
'
vs/platform/progress/common/progress
'
;
export
import
TestTextResourcePropertiesService
=
CommonWorkbenchTestServices
.
TestTextResourcePropertiesService
;
export
import
TestContextService
=
CommonWorkbenchTestServices
.
TestContextService
;
...
...
@@ -175,6 +176,7 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i
instantiationService
.
stub
(
IEnvironmentService
,
TestEnvironmentService
);
const
contextKeyService
=
<
IContextKeyService
>
instantiationService
.
createInstance
(
MockContextKeyService
);
instantiationService
.
stub
(
IContextKeyService
,
contextKeyService
);
instantiationService
.
stub
(
IProgressService
,
new
TestProgressService
());
const
workspaceContextService
=
new
TestContextService
(
TestWorkspace
);
instantiationService
.
stub
(
IWorkspaceContextService
,
workspaceContextService
);
const
configService
=
new
TestConfigurationService
();
...
...
@@ -217,6 +219,19 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i
return
instantiationService
;
}
export
class
TestProgressService
implements
IProgressService
{
_serviceBrand
:
undefined
;
withProgress
(
options
:
IProgressOptions
|
IProgressWindowOptions
|
IProgressNotificationOptions
|
IProgressCompositeOptions
,
task
:
(
progress
:
IProgress
<
IProgressStep
>
)
=>
Promise
<
any
>
,
onDidCancel
?:
((
choice
?:
number
|
undefined
)
=>
void
)
|
undefined
):
Promise
<
any
>
{
return
task
(
emptyProgress
);
}
}
export
class
TestAccessibilityService
implements
IAccessibilityService
{
_serviceBrand
:
undefined
;
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录