diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index 476ee9137a1..c98aa3770de 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -2,14 +2,14 @@ steps: - task: NodeTool@0 inputs: versionSpec: "12.13.0" +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.x" - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' -- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.x" + vstsFeed: 'vscode-build-cache' - script: | CHILD_CONCURRENCY=1 yarn --frozen-lockfile displayName: Install Dependencies @@ -18,7 +18,7 @@ steps: inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: 'vscode-build-cache' condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - script: | yarn electron x64 diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index a87e3753e77..0f611bd439d 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -10,14 +10,14 @@ steps: - task: NodeTool@0 inputs: versionSpec: "12.13.0" +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.x" - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' -- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.x" + vstsFeed: 'vscode-build-cache' - script: | CHILD_CONCURRENCY=1 yarn --frozen-lockfile displayName: Install Dependencies @@ -26,7 +26,7 @@ steps: inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: 'vscode-build-cache' condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - script: | yarn electron x64 diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index 7e16a00f0d8..9351bafa0bd 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -13,7 +13,7 @@ steps: inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: 'vscode-build-cache' - powershell: | yarn --frozen-lockfile env: @@ -24,7 +24,7 @@ steps: inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: 'vscode-build-cache' condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - powershell: | yarn electron diff --git a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts index 8dad613683c..5f209f327e4 100644 --- a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts +++ b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts @@ -58,7 +58,7 @@ class UpdateImportsOnFileRenameHandler extends Disposable { super(); this._register(vscode.workspace.onDidRenameFiles(async (e) => { - const [{ newUri, oldUri }] = e.renamed; + const [{ newUri, oldUri }] = e.files; const newFilePath = this.client.toPath(newUri); if (!newFilePath) { return; diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 4210ba2ba20..0d434f24ff6 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -141,4 +141,4 @@ export default class LanguageProvider extends Disposable { private get _diagnosticLanguage() { return this.description.diagnosticLanguage; } -} \ No newline at end of file +} diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts index 070b6818d72..96bbd3a0423 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts @@ -36,12 +36,56 @@ suite('workspace-event', () => { assert.ok(success); assert.ok(onWillCreate); - assert.equal(onWillCreate?.creating.length, 1); - assert.equal(onWillCreate?.creating[0].toString(), newUri.toString()); + assert.equal(onWillCreate?.files.length, 1); + assert.equal(onWillCreate?.files[0].toString(), newUri.toString()); assert.ok(onDidCreate); - assert.equal(onDidCreate?.created.length, 1); - assert.equal(onDidCreate?.created[0].toString(), newUri.toString()); + assert.equal(onDidCreate?.files.length, 1); + assert.equal(onDidCreate?.files[0].toString(), newUri.toString()); + }); + + test('onWillCreate/onDidCreate, make changes, edit another file', async function () { + + const base = await createRandomFile(); + const baseDoc = await vscode.workspace.openTextDocument(base); + + const newUri = base.with({ path: base.path + '-foo' }); + + disposables.push(vscode.workspace.onWillCreateFiles(e => { + const ws = new vscode.WorkspaceEdit(); + ws.insert(base, new vscode.Position(0, 0), 'HALLO_NEW'); + e.waitUntil(Promise.resolve(ws)); + })); + + const edit = new vscode.WorkspaceEdit(); + edit.createFile(newUri); + + const success = await vscode.workspace.applyEdit(edit); + assert.ok(success); + + assert.equal(baseDoc.getText(), 'HALLO_NEW'); + }); + + test('onWillCreate/onDidCreate, make changes, edit new file fails', async function () { + + const base = await createRandomFile(); + + const newUri = base.with({ path: base.path + '-foo' }); + + disposables.push(vscode.workspace.onWillCreateFiles(e => { + const ws = new vscode.WorkspaceEdit(); + ws.insert(e.files[0], new vscode.Position(0, 0), 'nope'); + e.waitUntil(Promise.resolve(ws)); + })); + + const edit = new vscode.WorkspaceEdit(); + edit.createFile(newUri); + + const success = await vscode.workspace.applyEdit(edit); + assert.ok(success); + + assert.equal((await vscode.workspace.fs.readFile(newUri)).toString(), ''); + assert.equal((await vscode.workspace.openTextDocument(newUri)).getText(), ''); }); test('onWillDelete/onDidDelete', async function () { @@ -61,12 +105,73 @@ suite('workspace-event', () => { assert.ok(success); assert.ok(onWilldelete); - assert.equal(onWilldelete?.deleting.length, 1); - assert.equal(onWilldelete?.deleting[0].toString(), base.toString()); + assert.equal(onWilldelete?.files.length, 1); + assert.equal(onWilldelete?.files[0].toString(), base.toString()); assert.ok(onDiddelete); - assert.equal(onDiddelete?.deleted.length, 1); - assert.equal(onDiddelete?.deleted[0].toString(), base.toString()); + assert.equal(onDiddelete?.files.length, 1); + assert.equal(onDiddelete?.files[0].toString(), base.toString()); + }); + + test('onWillDelete/onDidDelete, make changes', async function () { + + const base = await createRandomFile(); + const newUri = base.with({ path: base.path + '-NEW' }); + + disposables.push(vscode.workspace.onWillDeleteFiles(e => { + + const edit = new vscode.WorkspaceEdit(); + edit.createFile(newUri); + edit.insert(newUri, new vscode.Position(0, 0), 'hahah'); + e.waitUntil(Promise.resolve(edit)); + })); + + const edit = new vscode.WorkspaceEdit(); + edit.deleteFile(base); + + const success = await vscode.workspace.applyEdit(edit); + assert.ok(success); + }); + + test('onWillDelete/onDidDelete, make changes, del another file', async function () { + + const base = await createRandomFile(); + const base2 = await createRandomFile(); + disposables.push(vscode.workspace.onWillDeleteFiles(e => { + if (e.files[0].toString() === base.toString()) { + const edit = new vscode.WorkspaceEdit(); + edit.deleteFile(base2); + e.waitUntil(Promise.resolve(edit)); + } + })); + + const edit = new vscode.WorkspaceEdit(); + edit.deleteFile(base); + + const success = await vscode.workspace.applyEdit(edit); + assert.ok(success); + + + }); + + test('onWillDelete/onDidDelete, make changes, double delete', async function () { + + const base = await createRandomFile(); + let once = true; + disposables.push(vscode.workspace.onWillDeleteFiles(e => { + assert.ok(once); + once = false; + + const edit = new vscode.WorkspaceEdit(); + edit.deleteFile(e.files[0]); + e.waitUntil(Promise.resolve(edit)); + })); + + const edit = new vscode.WorkspaceEdit(); + edit.deleteFile(base); + + const success = await vscode.workspace.applyEdit(edit); + assert.ok(!success); }); test('onWillRename/onDidRename', async function () { @@ -87,14 +192,14 @@ suite('workspace-event', () => { assert.ok(success); assert.ok(onWillRename); - assert.equal(onWillRename?.renaming.length, 1); - assert.equal(onWillRename?.renaming[0].oldUri.toString(), oldUri.toString()); - assert.equal(onWillRename?.renaming[0].newUri.toString(), newUri.toString()); + assert.equal(onWillRename?.files.length, 1); + assert.equal(onWillRename?.files[0].oldUri.toString(), oldUri.toString()); + assert.equal(onWillRename?.files[0].newUri.toString(), newUri.toString()); assert.ok(onDidRename); - assert.equal(onDidRename?.renamed.length, 1); - assert.equal(onDidRename?.renamed[0].oldUri.toString(), oldUri.toString()); - assert.equal(onDidRename?.renamed[0].newUri.toString(), newUri.toString()); + assert.equal(onDidRename?.files.length, 1); + assert.equal(onDidRename?.files[0].oldUri.toString(), oldUri.toString()); + assert.equal(onDidRename?.files[0].newUri.toString(), newUri.toString()); }); test('onWillRename - make changes', async function () { @@ -109,7 +214,7 @@ suite('workspace-event', () => { disposables.push(vscode.workspace.onWillRenameFiles(e => { onWillRename = e; const edit = new vscode.WorkspaceEdit(); - edit.insert(e.renaming[0].oldUri, new vscode.Position(0, 0), 'FOO'); + edit.insert(e.files[0].oldUri, new vscode.Position(0, 0), 'FOO'); edit.replace(anotherFile, new vscode.Range(0, 0, 0, 3), 'FARBOO'); e.waitUntil(Promise.resolve(edit)); })); @@ -121,9 +226,9 @@ suite('workspace-event', () => { assert.ok(success); assert.ok(onWillRename); - assert.equal(onWillRename?.renaming.length, 1); - assert.equal(onWillRename?.renaming[0].oldUri.toString(), oldUri.toString()); - assert.equal(onWillRename?.renaming[0].newUri.toString(), newUri.toString()); + assert.equal(onWillRename?.files.length, 1); + assert.equal(onWillRename?.files[0].oldUri.toString(), oldUri.toString()); + assert.equal(onWillRename?.files[0].newUri.toString(), newUri.toString()); assert.equal((await vscode.workspace.openTextDocument(newUri)).getText(), 'FOOBAR'); assert.equal((await vscode.workspace.openTextDocument(anotherFile)).getText(), 'FARBOO'); diff --git a/package.json b/package.json index ac7ab9e99e9..b6f0986df88 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.41.0", - "distro": "403ab44be562c63a0cde1969fd8f5b45ff51709c", + "distro": "b2b71bcd54560577429f8ee10aa270a41036a09c", "author": { "name": "Microsoft Corporation" }, diff --git a/scripts/code-web.js b/scripts/code-web.js index 123d8261a9e..07c8700f7f9 100755 --- a/scripts/code-web.js +++ b/scripts/code-web.js @@ -20,17 +20,35 @@ const EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions'); const WEB_MAIN = path.join(APP_ROOT, 'src', 'vs', 'code', 'browser', 'workbench', 'workbench-dev.html'); const args = minimist(process.argv, { - string: [ + boolean: [ 'no-launch', - 'scheme', - 'host' + 'help' + ], + string: [ + 'scheme', + 'host', + 'port', + 'local_port' ], - number: [ - 'port' - ] }); +if (args.help) { + console.log( + 'yarn web [options]\n' + + ' --no-launch Do not open VSCode web in the browser\n' + + ' --scheme Protocol (https or http)\n' + + ' --host Remote host\n' + + ' --port Remote/Local port\n' + + ' --local_port Local port override\n' + + ' --help\n' + + '[Example]\n' + + ' yarn web --scheme https --host example.com --port 8080 --local_port 30000' + ); + process.exit(0); +} + const PORT = args.port || process.env.PORT || 8080; +const LOCAL_PORT = args.local_port || process.env.LOCAL_PORT || PORT; const SCHEME = args.scheme || process.env.VSCODE_SCHEME || 'http'; const HOST = args.host || 'localhost'; const AUTHORITY = process.env.VSCODE_AUTHORITY || `${HOST}:${PORT}`; @@ -65,8 +83,11 @@ const server = http.createServer((req, res) => { } }); -server.listen(PORT, () => { - console.log(`Web UI available at ${SCHEME}://${AUTHORITY}`); +server.listen(LOCAL_PORT, () => { + if (LOCAL_PORT !== PORT) { + console.log(`Operating location at http://0.0.0.0:${LOCAL_PORT}`); + } + console.log(`Web UI available at ${SCHEME}://${AUTHORITY}`); }); server.on('error', err => { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index f928b7dc2f7..5bc68faa7f1 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -706,12 +706,20 @@ export interface IAccessibilityProvider { export class DefaultStyleController implements IStyleController { - constructor(private styleElement: HTMLStyleElement, private selectorSuffix?: string) { } + constructor(private styleElement: HTMLStyleElement, private selectorSuffix: string) { } style(styles: IListStyles): void { - const suffix = this.selectorSuffix ? `.${this.selectorSuffix}` : ''; + const suffix = this.selectorSuffix && `.${this.selectorSuffix}`; const content: string[] = []; + if (styles.listBackground) { + if (styles.listBackground.isOpaque()) { + content.push(`.monaco-list${suffix} .monaco-list-rows { background: ${styles.listBackground}; }`); + } else { + console.warn(`List with id '${this.selectorSuffix}' was styled with a non-opaque background color. This will break sub-pixel antialiasing.`); + } + } + if (styles.listFocusBackground) { content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`); content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case! @@ -815,7 +823,7 @@ export class DefaultStyleController implements IStyleController { } } -export interface IListOptions extends IListStyles { +export interface IListOptions { readonly identityProvider?: IIdentityProvider; readonly dnd?: IListDragAndDrop; readonly enableKeyboardNavigation?: boolean; @@ -828,7 +836,7 @@ export interface IListOptions extends IListStyles { readonly multipleSelectionSupport?: boolean; readonly multipleSelectionController?: IMultipleSelectionController; readonly openController?: IOpenController; - readonly styleController?: IStyleController; + readonly styleController?: (suffix: string) => IStyleController; readonly accessibilityProvider?: IAccessibilityProvider; // list view options @@ -842,6 +850,7 @@ export interface IListOptions extends IListStyles { } export interface IListStyles { + listBackground?: Color; listFocusBackground?: Color; listFocusForeground?: Color; listActiveSelectionBackground?: Color; @@ -1103,7 +1112,6 @@ export class List implements ISpliceable, IDisposable { private eventBufferer = new EventBufferer(); private view: ListView; private spliceable: ISpliceable; - private styleElement: HTMLStyleElement; private styleController: IStyleController; private typeLabelController?: TypeLabelController; @@ -1210,9 +1218,12 @@ export class List implements ISpliceable, IDisposable { this.view.domNode.setAttribute('role', _options.ariaRole); } - this.styleElement = DOM.createStyleSheet(this.view.domNode); - - this.styleController = _options.styleController || new DefaultStyleController(this.styleElement, this.view.domId); + if (_options.styleController) { + this.styleController = _options.styleController(this.view.domId); + } else { + const styleElement = DOM.createStyleSheet(this.view.domNode); + this.styleController = new DefaultStyleController(styleElement, this.view.domId); + } this.spliceable = new CombinedSpliceable([ new TraitSpliceable(this.focus, this.view, _options.identityProvider), @@ -1249,8 +1260,6 @@ export class List implements ISpliceable, IDisposable { if (_options.ariaLabel) { this.view.domNode.setAttribute('aria-label', localize('aria list', "{0}. Use the navigation keys to navigate.", _options.ariaLabel)); } - - this.style(_options); } protected createMouseController(options: IListOptions): MouseController { diff --git a/src/vs/editor/common/core/lineTokens.ts b/src/vs/editor/common/core/lineTokens.ts index ffc1f05f49e..51aa6a93ca8 100644 --- a/src/vs/editor/common/core/lineTokens.ts +++ b/src/vs/editor/common/core/lineTokens.ts @@ -67,6 +67,11 @@ export class LineTokens implements IViewLineTokens { return 0; } + public getMetadata(tokenIndex: number): number { + const metadata = this._tokens[(tokenIndex << 1) + 1]; + return metadata; + } + public getLanguageId(tokenIndex: number): LanguageId { const metadata = this._tokens[(tokenIndex << 1) + 1]; return TokenMetadata.getLanguageId(metadata); @@ -132,8 +137,8 @@ export class LineTokens implements IViewLineTokens { while (low < high) { - let mid = low + Math.floor((high - low) / 2); - let endOffset = tokens[(mid << 1)]; + const mid = low + Math.floor((high - low) / 2); + const endOffset = tokens[(mid << 1)]; if (endOffset === desiredIndex) { return mid + 1; diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 25289e8e7f2..0c6a851ec53 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -14,7 +14,7 @@ import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChange import { SearchData } from 'vs/editor/common/model/textModelSearch'; import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; -import { MultilineTokens } from 'vs/editor/common/model/tokensStore'; +import { MultilineTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore'; /** * Vertical Lane in the overview ruler of the editor. @@ -792,6 +792,11 @@ export interface ITextModel { */ setTokens(tokens: MultilineTokens[]): void; + /** + * @internal + */ + setSemanticTokens(tokens: MultilineTokens2[] | null): void; + /** * Flush all tokenization state. * @internal diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index ff39a2066f6..adea9b60c7d 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -32,7 +32,7 @@ import { BracketsUtils, RichEditBracket, RichEditBrackets } from 'vs/editor/comm import { ITheme, ThemeColor } from 'vs/platform/theme/common/themeService'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { VSBufferReadableStream, VSBuffer } from 'vs/base/common/buffer'; -import { TokensStore, MultilineTokens, countEOL } from 'vs/editor/common/model/tokensStore'; +import { TokensStore, MultilineTokens, countEOL, MultilineTokens2, TokensStore2 } from 'vs/editor/common/model/tokensStore'; import { Color } from 'vs/base/common/color'; function createTextBufferBuilder() { @@ -276,6 +276,7 @@ export class TextModel extends Disposable implements model.ITextModel { private _languageIdentifier: LanguageIdentifier; private readonly _languageRegistryListener: IDisposable; private readonly _tokens: TokensStore; + private readonly _tokens2: TokensStore2; private readonly _tokenization: TextModelTokenization; //#endregion @@ -339,6 +340,7 @@ export class TextModel extends Disposable implements model.ITextModel { this._trimAutoWhitespaceLines = null; this._tokens = new TokensStore(); + this._tokens2 = new TokensStore2(); this._tokenization = new TextModelTokenization(this); } @@ -414,6 +416,7 @@ export class TextModel extends Disposable implements model.ITextModel { // Flush all tokens this._tokens.flush(); + this._tokens2.flush(); // Destroy all my decorations this._decorations = Object.create(null); @@ -1262,8 +1265,9 @@ export class TextModel extends Disposable implements model.ITextModel { let lineCount = oldLineCount; for (let i = 0, len = contentChanges.length; i < len; i++) { const change = contentChanges[i]; - const [eolCount, firstLineLength] = countEOL(change.text); + const [eolCount, firstLineLength, lastLineLength] = countEOL(change.text); this._tokens.acceptEdit(change.range, eolCount, firstLineLength); + this._tokens2.acceptEdit(change.range, eolCount, firstLineLength, lastLineLength, change.text.length > 0 ? change.text.charCodeAt(0) : CharCode.Null); this._onDidChangeDecorations.fire(); this._decorationsTree.acceptReplace(change.rangeOffset, change.rangeLength, change.text.length, change.forceMoveMarkers); @@ -1717,6 +1721,15 @@ export class TextModel extends Disposable implements model.ITextModel { }); } + public setSemanticTokens(tokens: MultilineTokens2[] | null): void { + this._tokens2.set(tokens); + + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + ranges: [{ fromLineNumber: 1, toLineNumber: this.getLineCount() }] + }); + } + public tokenizeViewport(startLineNumber: number, endLineNumber: number): void { startLineNumber = Math.max(1, startLineNumber); endLineNumber = Math.min(this._buffer.getLineCount(), endLineNumber); @@ -1734,6 +1747,15 @@ export class TextModel extends Disposable implements model.ITextModel { }); } + public clearSemanticTokens(): void { + this._tokens2.flush(); + + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + ranges: [{ fromLineNumber: 1, toLineNumber: this.getLineCount() }] + }); + } + private _emitModelTokensChangedEvent(e: IModelTokensChangedEvent): void { if (!this._isDisposing) { this._onDidChangeTokens.fire(e); @@ -1772,7 +1794,8 @@ export class TextModel extends Disposable implements model.ITextModel { private _getLineTokens(lineNumber: number): LineTokens { const lineText = this.getLineContent(lineNumber); - return this._tokens.getTokens(this._languageIdentifier.id, lineNumber - 1, lineText); + const syntacticTokens = this._tokens.getTokens(this._languageIdentifier.id, lineNumber - 1, lineText); + return this._tokens2.addSemanticTokens(lineNumber, syntacticTokens); } public getLanguageIdentifier(): LanguageIdentifier { diff --git a/src/vs/editor/common/model/tokensStore.ts b/src/vs/editor/common/model/tokensStore.ts index d965bcf8937..aa6953853ca 100644 --- a/src/vs/editor/common/model/tokensStore.ts +++ b/src/vs/editor/common/model/tokensStore.ts @@ -11,9 +11,10 @@ import { ColorId, FontStyle, LanguageId, MetadataConsts, StandardTokenType, Toke import { writeUInt32BE, readUInt32BE } from 'vs/base/common/buffer'; import { CharCode } from 'vs/base/common/charCode'; -export function countEOL(text: string): [number, number] { +export function countEOL(text: string): [number, number, number] { let eolCount = 0; let firstLineLength = 0; + let lastLineStart = 0; for (let i = 0, len = text.length; i < len; i++) { const chr = text.charCodeAt(i); @@ -28,17 +29,19 @@ export function countEOL(text: string): [number, number] { } else { // \r... case } + lastLineStart = i + 1; } else if (chr === CharCode.LineFeed) { if (eolCount === 0) { firstLineLength = i; } eolCount++; + lastLineStart = i + 1; } } if (eolCount === 0) { firstLineLength = text.length; } - return [eolCount, firstLineLength]; + return [eolCount, firstLineLength, text.length - lastLineStart]; } function getDefaultMetadata(topLevelLanguageId: LanguageId): number { @@ -109,6 +112,453 @@ export class MultilineTokensBuilder { } } +export interface IEncodedTokens { + getTokenCount(): number; + getDeltaLine(tokenIndex: number): number; + getMaxDeltaLine(): number; + getStartCharacter(tokenIndex: number): number; + getEndCharacter(tokenIndex: number): number; + getMetadata(tokenIndex: number): number; + + clear(): void; + acceptDeleteRange(startDeltaLine: number, startCharacter: number, endDeltaLine: number, endCharacter: number): void; + acceptInsertText(deltaLine: number, character: number, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void; +} + +export class SparseEncodedTokens implements IEncodedTokens { + /** + * The encoding of tokens is: + * 4*i deltaLine (from `startLineNumber`) + * 4*i+1 startCharacter (from the line start) + * 4*i+2 endCharacter (from the line start) + * 4*i+3 metadata + */ + private _tokens: Uint32Array; + private _tokenCount: number; + + constructor(tokens: Uint32Array) { + this._tokens = tokens; + this._tokenCount = tokens.length / 4; + } + + public getMaxDeltaLine(): number { + const tokenCount = this.getTokenCount(); + if (tokenCount === 0) { + return -1; + } + return this.getDeltaLine(tokenCount - 1); + } + + public getTokenCount(): number { + return this._tokenCount; + } + + public getDeltaLine(tokenIndex: number): number { + return this._tokens[4 * tokenIndex]; + } + + public getStartCharacter(tokenIndex: number): number { + return this._tokens[4 * tokenIndex + 1]; + } + + public getEndCharacter(tokenIndex: number): number { + return this._tokens[4 * tokenIndex + 2]; + } + + public getMetadata(tokenIndex: number): number { + return this._tokens[4 * tokenIndex + 3]; + } + + public clear(): void { + this._tokenCount = 0; + } + + public acceptDeleteRange(startDeltaLine: number, startCharacter: number, endDeltaLine: number, endCharacter: number): void { + // This is a bit complex, here are the cases I used to think about this: + // + // 1. The token starts before the deletion range + // 1a. The token is completely before the deletion range + // ----------- + // xxxxxxxxxxx + // 1b. The token starts before, the deletion range ends after the token + // ----------- + // xxxxxxxxxxx + // 1c. The token starts before, the deletion range ends precisely with the token + // --------------- + // xxxxxxxx + // 1d. The token starts before, the deletion range is inside the token + // --------------- + // xxxxx + // + // 2. The token starts at the same position with the deletion range + // 2a. The token starts at the same position, and ends inside the deletion range + // ------- + // xxxxxxxxxxx + // 2b. The token starts at the same position, and ends at the same position as the deletion range + // ---------- + // xxxxxxxxxx + // 2c. The token starts at the same position, and ends after the deletion range + // ------------- + // xxxxxxx + // + // 3. The token starts inside the deletion range + // 3a. The token is inside the deletion range + // ------- + // xxxxxxxxxxxxx + // 3b. The token starts inside the deletion range, and ends at the same position as the deletion range + // ---------- + // xxxxxxxxxxxxx + // 3c. The token starts inside the deletion range, and ends after the deletion range + // ------------ + // xxxxxxxxxxx + // + // 4. The token starts after the deletion range + // ----------- + // xxxxxxxx + // + const tokens = this._tokens; + const tokenCount = this._tokenCount; + const deletedLineCount = (endDeltaLine - startDeltaLine); + let newTokenCount = 0; + let hasDeletedTokens = false; + for (let i = 0; i < tokenCount; i++) { + const srcOffset = 4 * i; + let tokenDeltaLine = tokens[srcOffset]; + let tokenStartCharacter = tokens[srcOffset + 1]; + let tokenEndCharacter = tokens[srcOffset + 2]; + const tokenMetadata = tokens[srcOffset + 3]; + + if (tokenDeltaLine < startDeltaLine || (tokenDeltaLine === startDeltaLine && tokenEndCharacter <= startCharacter)) { + // 1a. The token is completely before the deletion range + // => nothing to do + newTokenCount++; + continue; + } else if (tokenDeltaLine === startDeltaLine && tokenStartCharacter < startCharacter) { + // 1b, 1c, 1d + // => the token survives, but it needs to shrink + if (tokenDeltaLine === endDeltaLine && tokenEndCharacter > endCharacter) { + // 1d. The token starts before, the deletion range is inside the token + // => the token shrinks by the deletion character count + tokenEndCharacter -= (endCharacter - startCharacter); + } else { + // 1b. The token starts before, the deletion range ends after the token + // 1c. The token starts before, the deletion range ends precisely with the token + // => the token shrinks its ending to the deletion start + tokenEndCharacter = startCharacter; + } + } else if (tokenDeltaLine === startDeltaLine && tokenStartCharacter === startCharacter) { + // 2a, 2b, 2c + if (tokenDeltaLine === endDeltaLine && tokenEndCharacter > endCharacter) { + // 2c. The token starts at the same position, and ends after the deletion range + // => the token shrinks by the deletion character count + tokenEndCharacter -= (endCharacter - startCharacter); + } else { + // 2a. The token starts at the same position, and ends inside the deletion range + // 2b. The token starts at the same position, and ends at the same position as the deletion range + // => the token is deleted + hasDeletedTokens = true; + continue; + } + } else if (tokenDeltaLine < endDeltaLine || (tokenDeltaLine === endDeltaLine && tokenStartCharacter < endCharacter)) { + // 3a, 3b, 3c + if (tokenDeltaLine === endDeltaLine && tokenEndCharacter > endCharacter) { + // 3c. The token starts inside the deletion range, and ends after the deletion range + // => the token moves left and shrinks + if (tokenDeltaLine === startDeltaLine) { + // the deletion started on the same line as the token + // => the token moves left and shrinks + tokenStartCharacter = startCharacter; + tokenEndCharacter = tokenStartCharacter + (tokenEndCharacter - endCharacter); + } else { + // the deletion started on a line above the token + // => the token moves to the beginning of the line + tokenStartCharacter = 0; + tokenEndCharacter = tokenStartCharacter + (tokenEndCharacter - endCharacter); + } + } else { + // 3a. The token is inside the deletion range + // 3b. The token starts inside the deletion range, and ends at the same position as the deletion range + // => the token is deleted + hasDeletedTokens = true; + continue; + } + } else if (tokenDeltaLine > endDeltaLine) { + // 4. (partial) The token starts after the deletion range, on a line below... + if (deletedLineCount === 0 && !hasDeletedTokens) { + // early stop, there is no need to walk all the tokens and do nothing... + newTokenCount = tokenCount; + break; + } + tokenDeltaLine -= deletedLineCount; + } else if (tokenDeltaLine === endDeltaLine && tokenStartCharacter >= endCharacter) { + // 4. (continued) The token starts after the deletion range, on the last line where a deletion occurs + tokenDeltaLine -= deletedLineCount; + tokenStartCharacter -= endCharacter; + tokenEndCharacter -= endCharacter; + } else { + throw new Error(`Not possible!`); + } + + const destOffset = 4 * newTokenCount; + tokens[destOffset] = tokenDeltaLine; + tokens[destOffset + 1] = tokenStartCharacter; + tokens[destOffset + 2] = tokenEndCharacter; + tokens[destOffset + 3] = tokenMetadata; + newTokenCount++; + } + + this._tokenCount = newTokenCount; + } + + public acceptInsertText(deltaLine: number, character: number, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void { + // Here are the cases I used to think about this: + // + // 1. The token is completely before the insertion point + // ----------- | + // 2. The token ends precisely at the insertion point + // -----------| + // 3. The token contains the insertion point + // -----|------ + // 4. The token starts precisely at the insertion point + // |----------- + // 5. The token is completely after the insertion point + // | ----------- + // + const isInsertingPreciselyOneWordCharacter = ( + eolCount === 0 + && firstLineLength === 1 + && ( + (firstCharCode >= CharCode.Digit0 && firstCharCode <= CharCode.Digit9) + || (firstCharCode >= CharCode.A && firstCharCode <= CharCode.Z) + || (firstCharCode >= CharCode.a && firstCharCode <= CharCode.z) + ) + ); + const tokens = this._tokens; + const tokenCount = this._tokenCount; + for (let i = 0; i < tokenCount; i++) { + const offset = 4 * i; + let tokenDeltaLine = tokens[offset]; + let tokenStartCharacter = tokens[offset + 1]; + let tokenEndCharacter = tokens[offset + 2]; + + if (tokenDeltaLine < deltaLine || (tokenDeltaLine === deltaLine && tokenEndCharacter < character)) { + // 1. The token is completely before the insertion point + // => nothing to do + continue; + } else if (tokenDeltaLine === deltaLine && tokenEndCharacter === character) { + // 2. The token ends precisely at the insertion point + // => expand the end character only if inserting precisely one character that is a word character + if (isInsertingPreciselyOneWordCharacter) { + tokenEndCharacter += 1; + } else { + continue; + } + } else if (tokenDeltaLine === deltaLine && tokenStartCharacter < character && character < tokenEndCharacter) { + // 3. The token contains the insertion point + if (eolCount === 0) { + // => just expand the end character + tokenEndCharacter += firstLineLength; + } else { + // => cut off the token + tokenEndCharacter = character; + } + } else { + // 4. or 5. + if (tokenDeltaLine === deltaLine && tokenStartCharacter === character) { + // 4. The token starts precisely at the insertion point + // => grow the token (by keeping its start constant) only if inserting precisely one character that is a word character + // => otherwise behave as in case 5. + if (isInsertingPreciselyOneWordCharacter) { + continue; + } + } + // => the token must move and keep its size constant + tokenDeltaLine += eolCount; + if (tokenDeltaLine === deltaLine) { + // this token is on the line where the insertion is taking place + if (eolCount === 0) { + tokenStartCharacter += firstLineLength; + tokenEndCharacter += firstLineLength; + } else { + const tokenLength = tokenEndCharacter - tokenStartCharacter; + tokenStartCharacter = lastLineLength + (tokenStartCharacter - character); + tokenEndCharacter = tokenStartCharacter + tokenLength; + } + } + } + + tokens[offset] = tokenDeltaLine; + tokens[offset + 1] = tokenStartCharacter; + tokens[offset + 2] = tokenEndCharacter; + } + } +} + +export class LineTokens2 { + + private readonly _actual: IEncodedTokens; + private readonly _startTokenIndex: number; + private readonly _endTokenIndex: number; + + constructor(actual: IEncodedTokens, startTokenIndex: number, endTokenIndex: number) { + this._actual = actual; + this._startTokenIndex = startTokenIndex; + this._endTokenIndex = endTokenIndex; + } + + public getCount(): number { + return this._endTokenIndex - this._startTokenIndex + 1; + } + + public getStartCharacter(tokenIndex: number): number { + return this._actual.getStartCharacter(this._startTokenIndex + tokenIndex); + } + + public getEndCharacter(tokenIndex: number): number { + return this._actual.getEndCharacter(this._startTokenIndex + tokenIndex); + } + + public getMetadata(tokenIndex: number): number { + return this._actual.getMetadata(this._startTokenIndex + tokenIndex); + } +} + +export class MultilineTokens2 { + + public startLineNumber: number; + public endLineNumber: number; + public tokens: IEncodedTokens; + + constructor(startLineNumber: number, tokens: IEncodedTokens) { + this.startLineNumber = startLineNumber; + this.tokens = tokens; + this.endLineNumber = this.startLineNumber + this.tokens.getMaxDeltaLine(); + } + + private _updateEndLineNumber(): void { + this.endLineNumber = this.startLineNumber + this.tokens.getMaxDeltaLine(); + } + + public getLineTokens(lineNumber: number): LineTokens2 | null { + if (this.startLineNumber <= lineNumber && lineNumber <= this.endLineNumber) { + const findResult = MultilineTokens2._findTokensWithLine(this.tokens, lineNumber - this.startLineNumber); + if (findResult) { + const [startTokenIndex, endTokenIndex] = findResult; + return new LineTokens2(this.tokens, startTokenIndex, endTokenIndex); + } + } + return null; + } + + private static _findTokensWithLine(tokens: IEncodedTokens, deltaLine: number): [number, number] | null { + let low = 0; + let high = tokens.getTokenCount() - 1; + + while (low < high) { + const mid = low + Math.floor((high - low) / 2); + const midDeltaLine = tokens.getDeltaLine(mid); + + if (midDeltaLine < deltaLine) { + low = mid + 1; + } else if (midDeltaLine > deltaLine) { + high = mid - 1; + } else { + let min = mid; + while (min > low && tokens.getDeltaLine(min - 1) === deltaLine) { + min--; + } + let max = mid; + while (max < high && tokens.getDeltaLine(max + 1) === deltaLine) { + max++; + } + return [min, max]; + } + } + + if (tokens.getDeltaLine(low) === deltaLine) { + return [low, low]; + } + + return null; + } + + public applyEdit(range: IRange, text: string): void { + const [eolCount, firstLineLength, lastLineLength] = countEOL(text); + this.acceptEdit(range, eolCount, firstLineLength, lastLineLength, text.length > 0 ? text.charCodeAt(0) : CharCode.Null); + } + + public acceptEdit(range: IRange, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void { + this._acceptDeleteRange(range); + this._acceptInsertText(new Position(range.startLineNumber, range.startColumn), eolCount, firstLineLength, lastLineLength, firstCharCode); + this._updateEndLineNumber(); + } + + private _acceptDeleteRange(range: IRange): void { + if (range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn) { + // Nothing to delete + return; + } + + const firstLineIndex = range.startLineNumber - this.startLineNumber; + const lastLineIndex = range.endLineNumber - this.startLineNumber; + + if (lastLineIndex < 0) { + // this deletion occurs entirely before this block, so we only need to adjust line numbers + const deletedLinesCount = lastLineIndex - firstLineIndex; + this.startLineNumber -= deletedLinesCount; + return; + } + + const tokenMaxDeltaLine = this.tokens.getMaxDeltaLine(); + + if (firstLineIndex >= tokenMaxDeltaLine + 1) { + // this deletion occurs entirely after this block, so there is nothing to do + return; + } + + if (firstLineIndex < 0 && lastLineIndex >= tokenMaxDeltaLine + 1) { + // this deletion completely encompasses this block + this.startLineNumber = 0; + this.tokens.clear(); + return; + } + + if (firstLineIndex < 0) { + const deletedBefore = -firstLineIndex; + this.startLineNumber -= deletedBefore; + + this.tokens.acceptDeleteRange(0, 0, lastLineIndex, range.endColumn - 1); + } else { + this.tokens.acceptDeleteRange(firstLineIndex, range.startColumn - 1, lastLineIndex, range.endColumn - 1); + } + } + + private _acceptInsertText(position: Position, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void { + + if (eolCount === 0 && firstLineLength === 0) { + // Nothing to insert + return; + } + + const lineIndex = position.lineNumber - this.startLineNumber; + + if (lineIndex < 0) { + // this insertion occurs before this block, so we only need to adjust line numbers + this.startLineNumber += eolCount; + return; + } + + const tokenMaxDeltaLine = this.tokens.getMaxDeltaLine(); + + if (lineIndex >= tokenMaxDeltaLine + 1) { + // this insertion occurs after this block, so there is nothing to do + return; + } + + this.tokens.acceptInsertText(lineIndex, position.column - 1, eolCount, firstLineLength, lastLineLength, firstCharCode); + } +} + export class MultilineTokens { public startLineNumber: number; @@ -193,6 +643,7 @@ export class MultilineTokens { // this deletion completely encompasses this block this.startLineNumber = 0; this.tokens = []; + return; } if (firstLineIndex === lastLineIndex) { @@ -289,6 +740,120 @@ function toUint32Array(arr: Uint32Array | ArrayBuffer): Uint32Array { } } +export class TokensStore2 { + + private _pieces: MultilineTokens2[]; + + constructor() { + this._pieces = []; + } + + public flush(): void { + this._pieces = []; + } + + public set(pieces: MultilineTokens2[] | null) { + this._pieces = pieces || []; + } + + public addSemanticTokens(lineNumber: number, aTokens: LineTokens): LineTokens { + const pieces = this._pieces; + + if (pieces.length === 0) { + return aTokens; + } + + const pieceIndex = TokensStore2._findFirstPieceWithLine(pieces, lineNumber); + const bTokens = this._pieces[pieceIndex].getLineTokens(lineNumber); + + if (!bTokens) { + return aTokens; + } + + const aLen = aTokens.getCount(); + const bLen = bTokens.getCount(); + + let aIndex = 0; + let result: number[] = [], resultLen = 0; + for (let bIndex = 0; bIndex < bLen; bIndex++) { + const bStartCharacter = bTokens.getStartCharacter(bIndex); + const bEndCharacter = bTokens.getEndCharacter(bIndex); + const bMetadata = bTokens.getMetadata(bIndex); + + // push any token from `a` that is before `b` + while (aIndex < aLen && aTokens.getEndOffset(aIndex) <= bStartCharacter) { + result[resultLen++] = aTokens.getEndOffset(aIndex); + result[resultLen++] = aTokens.getMetadata(aIndex); + aIndex++; + } + + // push the token from `a` if it intersects the token from `b` + if (aIndex < aLen && aTokens.getStartOffset(aIndex) < bStartCharacter) { + result[resultLen++] = bStartCharacter; + result[resultLen++] = aTokens.getMetadata(aIndex); + } + + // skip any tokens from `a` that are contained inside `b` + while (aIndex < aLen && aTokens.getEndOffset(aIndex) <= bEndCharacter) { + aIndex++; + } + + const aMetadata = aTokens.getMetadata(aIndex - 1 > 0 ? aIndex - 1 : aIndex); + const languageId = TokenMetadata.getLanguageId(aMetadata); + const tokenType = TokenMetadata.getTokenType(aMetadata); + + // push the token from `b` + result[resultLen++] = bEndCharacter; + result[resultLen++] = ( + (bMetadata & MetadataConsts.LANG_TTYPE_CMPL) + | ((languageId << MetadataConsts.LANGUAGEID_OFFSET) >>> 0) + | ((tokenType << MetadataConsts.TOKEN_TYPE_OFFSET) >>> 0) + ); + } + + // push the remaining tokens from `a` + while (aIndex < aLen) { + result[resultLen++] = aTokens.getEndOffset(aIndex); + result[resultLen++] = aTokens.getMetadata(aIndex); + aIndex++; + } + + return new LineTokens(new Uint32Array(result), aTokens.getLineContent()); + } + + private static _findFirstPieceWithLine(pieces: MultilineTokens2[], lineNumber: number): number { + let low = 0; + let high = pieces.length - 1; + + while (low < high) { + let mid = low + Math.floor((high - low) / 2); + + if (pieces[mid].endLineNumber < lineNumber) { + low = mid + 1; + } else if (pieces[mid].startLineNumber > lineNumber) { + high = mid - 1; + } else { + while (mid > low && pieces[mid - 1].startLineNumber <= lineNumber && lineNumber <= pieces[mid - 1].endLineNumber) { + mid--; + } + return mid; + } + } + + return low; + } + + //#region Editing + + public acceptEdit(range: IRange, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void { + for (const piece of this._pieces) { + piece.acceptEdit(range, eolCount, firstLineLength, lastLineLength, firstCharCode); + } + } + + //#endregion +} + export class TokensStore { private _lineTokens: (Uint32Array | ArrayBuffer | null)[]; private _len: number; diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 67a01dd72cc..4fd62bcd685 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -125,6 +125,8 @@ export const enum MetadataConsts { FOREGROUND_MASK = 0b00000000011111111100000000000000, BACKGROUND_MASK = 0b11111111100000000000000000000000, + LANG_TTYPE_CMPL = 0b11111111111111111111100000000000, + LANGUAGEID_OFFSET = 0, TOKEN_TYPE_OFFSET = 8, FONT_STYLE_OFFSET = 11, @@ -1462,6 +1464,33 @@ export interface CodeLensProvider { resolveCodeLens?(model: model.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } +export interface SemanticColoringLegend { + readonly tokenTypes: string[]; + readonly tokenModifiers: string[]; +} + +export interface SemanticColoringArea { + /** + * The zero-based line value where this token block begins. + */ + readonly line: number; + /** + * The actual token block encoded data. + */ + readonly data: Uint32Array; + +} + +export interface SemanticColoring { + readonly areas: SemanticColoringArea[]; + dispose(): void; +} + +export interface SemanticColoringProvider { + getLegend(): SemanticColoringLegend; + provideSemanticColoring(model: model.ITextModel, token: CancellationToken): ProviderResult; +} + // --- feature registries ------ /** @@ -1564,6 +1593,11 @@ export const SelectionRangeRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const SemanticColoringProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 5b5070184ea..6fbe5274a87 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -6,19 +6,23 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; +import * as errors from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model'; import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; -import { IModelLanguageChangedEvent } from 'vs/editor/common/model/textModelEvents'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; +import { IModelLanguageChangedEvent, IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { LanguageIdentifier, SemanticColoringProviderRegistry, SemanticColoringProvider, SemanticColoring, FontStyle, MetadataConsts } from 'vs/editor/common/modes'; import { PLAINTEXT_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/modesRegistry'; import { ILanguageSelection } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { SparseEncodedTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -124,6 +128,8 @@ export class ModelServiceImpl extends Disposable implements IModelService { this._configurationServiceSubscription = this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions()); this._updateModelOptions(); + + this._register(new SemanticColoringFeature(this)); } private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions { @@ -430,3 +436,160 @@ export class ModelServiceImpl extends Disposable implements IModelService { export interface ILineSequence { getLineContent(lineNumber: number): string; } + +class SemanticColoringFeature extends Disposable { + private _watchers: Record; + + constructor(modelService: IModelService) { + super(); + this._watchers = Object.create(null); + this._register(modelService.onModelAdded((model) => { + this._watchers[model.uri.toString()] = new ModelSemanticColoring(model); + })); + this._register(modelService.onModelRemoved((model) => { + this._watchers[model.uri.toString()].dispose(); + delete this._watchers[model.uri.toString()]; + })); + } +} + +class ModelSemanticColoring extends Disposable { + + private _isDisposed: boolean; + private readonly _model: ITextModel; + private readonly _fetchSemanticTokens: RunOnceScheduler; + private _currentResponse: SemanticColoring | null; + private _currentRequestCancellationTokenSource: CancellationTokenSource | null; + + constructor(model: ITextModel) { + super(); + + this._isDisposed = false; + this._model = model; + this._fetchSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchSemanticTokensNow(), 500)); + this._currentResponse = null; + this._currentRequestCancellationTokenSource = null; + + this._register(this._model.onDidChangeContent(e => this._fetchSemanticTokens.schedule())); + this._register(SemanticColoringProviderRegistry.onDidChange(e => this._fetchSemanticTokens.schedule())); + this._fetchSemanticTokens.schedule(0); + } + + public dispose(): void { + this._isDisposed = true; + if (this._currentResponse) { + this._currentResponse.dispose(); + this._currentResponse = null; + } + if (this._currentRequestCancellationTokenSource) { + this._currentRequestCancellationTokenSource.cancel(); + this._currentRequestCancellationTokenSource = null; + } + super.dispose(); + } + + private _fetchSemanticTokensNow(): void { + if (this._currentRequestCancellationTokenSource) { + // there is already a request running, let it finish... + return; + } + const provider = this._getSemanticColoringProvider(); + if (!provider) { + return; + } + this._currentRequestCancellationTokenSource = new CancellationTokenSource(); + + const pendingChanges: IModelContentChangedEvent[] = []; + const contentChangeListener = this._model.onDidChangeContent((e) => { + pendingChanges.push(e); + }); + + const request = Promise.resolve(provider.provideSemanticColoring(this._model, this._currentRequestCancellationTokenSource.token)); + + request.then((res) => { + this._currentRequestCancellationTokenSource = null; + contentChangeListener.dispose(); + this._setSemanticTokens(res || null, pendingChanges); + }, (err) => { + errors.onUnexpectedError(err); + this._currentRequestCancellationTokenSource = null; + contentChangeListener.dispose(); + this._setSemanticTokens(null, pendingChanges); + }); + } + + private _setSemanticTokens(tokens: SemanticColoring | null, pendingChanges: IModelContentChangedEvent[]): void { + if (this._currentResponse) { + this._currentResponse.dispose(); + this._currentResponse = null; + } + if (this._isDisposed) { + // disposed! + if (tokens) { + tokens.dispose(); + } + return; + } + this._currentResponse = tokens; + if (!this._currentResponse) { + this._model.setSemanticTokens(null); + return; + } + + const result: MultilineTokens2[] = []; + for (const area of this._currentResponse.areas) { + const srcTokens = area.data; + const tokenCount = srcTokens.length / 5; + const destTokens = new Uint32Array(4 * tokenCount); + for (let i = 0; i < tokenCount; i++) { + const srcOffset = 5 * i; + const deltaLine = srcTokens[srcOffset]; + const startCharacter = srcTokens[srcOffset + 1]; + const endCharacter = srcTokens[srcOffset + 2]; + // const tokenType = srcTokens[srcOffset + 3]; + // const tokenModifiers = srcTokens[srcOffset + 4]; + // TODO@semantic: map here tokenType and tokenModifiers to metadata + + const fontStyle = FontStyle.Italic | FontStyle.Bold | FontStyle.Underline; + const foregroundColorId = 3; + const metadata = ( + (fontStyle << MetadataConsts.FONT_STYLE_OFFSET) + | (foregroundColorId << MetadataConsts.FOREGROUND_OFFSET) + ) >>> 0; + + const destOffset = 4 * i; + destTokens[destOffset] = deltaLine; + destTokens[destOffset + 1] = startCharacter; + destTokens[destOffset + 2] = endCharacter; + destTokens[destOffset + 3] = metadata; + } + + const tokens = new MultilineTokens2(area.line, new SparseEncodedTokens(destTokens)); + result.push(tokens); + } + + // Adjust incoming semantic tokens + if (pendingChanges.length > 0) { + // More changes occurred while the request was running + // We need to: + // 1. Adjust incoming semantic tokens + // 2. Request them again + for (const change of pendingChanges) { + for (const area of result) { + for (const singleChange of change.changes) { + area.applyEdit(singleChange.range, singleChange.text); + } + } + } + + this._fetchSemanticTokens.schedule(); + } + + this._model.setSemanticTokens(result); + } + + private _getSemanticColoringProvider(): SemanticColoringProvider | null { + const result = SemanticColoringProviderRegistry.ordered(this._model); + return (result.length > 0 ? result[0] : null); + } +} diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 6c259a207c0..852571ac070 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -223,9 +223,16 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { } })); this._localToDispose.add(this._editor.onMouseUp(e => { - if (e.target.type === editorBrowser.MouseTargetType.CONTENT_WIDGET && e.target.element && e.target.element.tagName === 'A') { + if (e.target.type !== editorBrowser.MouseTargetType.CONTENT_WIDGET) { + return; + } + let target = e.target.element; + if (target?.tagName === 'SPAN') { + target = target.parentElement; + } + if (target?.tagName === 'A') { for (const lens of this._lenses) { - let command = lens.getCommand(e.target.element as HTMLLinkElement); + let command = lens.getCommand(target as HTMLLinkElement); if (command) { this._commandService.executeCommand(command.id, ...(command.arguments || [])).catch(err => this._notificationService.error(err)); break; @@ -293,7 +300,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { groupsIndex++; codeLensIndex++; } else { - this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); + this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); codeLensIndex++; groupsIndex++; } @@ -307,7 +314,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { // Create extra symbols while (groupsIndex < groups.length) { - this._lenses.push(new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); + this._lenses.push(new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); groupsIndex++; } diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index 54318b98678..c046279ce16 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -56,26 +56,24 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { private readonly _id: string; private readonly _domNode: HTMLElement; - private readonly _editor: editorBrowser.ICodeEditor; + private readonly _editor: editorBrowser.IActiveCodeEditor; private readonly _commands = new Map(); private _widgetPosition?: editorBrowser.IContentWidgetPosition; private _isEmpty: boolean = true; constructor( - editor: editorBrowser.ICodeEditor, + editor: editorBrowser.IActiveCodeEditor, className: string, - symbolRange: Range, - lenses: Array + line: number, ) { this._editor = editor; this._id = (CodeLensContentWidget._idPool++).toString(); - this.setSymbolRange(symbolRange); + this.updatePosition(line); this._domNode = document.createElement('span'); this._domNode.className = `codelens-decoration ${className}`; - this.withCommands(lenses, false); } withCommands(lenses: Array, animate: boolean): void { @@ -113,7 +111,6 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { innerHtml = ' '; } this._domNode.innerHTML = innerHtml; - this._editor.layoutContentWidget(this); if (this._isEmpty && animate) { dom.addClass(this._domNode, 'fadein'); } @@ -135,14 +132,10 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { return this._domNode; } - setSymbolRange(range: Range): void { - if (!this._editor.hasModel()) { - return; - } - const lineNumber = range.startLineNumber; - const column = this._editor.getModel().getLineFirstNonWhitespaceColumn(lineNumber); + updatePosition(line: number): void { + const column = this._editor.getModel().getLineFirstNonWhitespaceColumn(line); this._widgetPosition = { - position: { lineNumber: lineNumber, column: column }, + position: { lineNumber: line, column: column }, preference: [editorBrowser.ContentWidgetPositionPreference.ABOVE] }; } @@ -150,10 +143,6 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { getPosition(): editorBrowser.IContentWidgetPosition | null { return this._widgetPosition || null; } - - isVisible(): boolean { - return this._domNode.hasAttribute('monaco-visible-content-widget'); - } } export interface IDecorationIdCallback { @@ -191,30 +180,38 @@ export class CodeLensHelper { export class CodeLensWidget { - private readonly _editor: editorBrowser.ICodeEditor; + private readonly _editor: editorBrowser.IActiveCodeEditor; + private readonly _className: string; private readonly _viewZone!: CodeLensViewZone; private readonly _viewZoneId!: string; - private readonly _contentWidget!: CodeLensContentWidget; + + private _contentWidget?: CodeLensContentWidget; private _decorationIds: string[]; private _data: CodeLensItem[]; constructor( data: CodeLensItem[], - editor: editorBrowser.ICodeEditor, + editor: editorBrowser.IActiveCodeEditor, className: string, helper: CodeLensHelper, viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor, updateCallback: Function ) { this._editor = editor; + this._className = className; this._data = data; - this._decorationIds = new Array(this._data.length); + // create combined range, track all ranges with decorations, + // check if there is already something to render + this._decorationIds = []; let range: Range | undefined; let lenses: CodeLens[] = []; + this._data.forEach((codeLensData, i) => { - lenses.push(codeLensData.symbol); + if (codeLensData.symbol.command) { + lenses.push(codeLensData.symbol); + } helper.addDecoration({ range: codeLensData.symbol.range, @@ -229,43 +226,45 @@ export class CodeLensWidget { } }); - if (range) { - this._contentWidget = new CodeLensContentWidget(editor, className, range, lenses); - this._viewZone = new CodeLensViewZone(range.startLineNumber - 1, updateCallback); + this._viewZone = new CodeLensViewZone(range!.startLineNumber - 1, updateCallback); + this._viewZoneId = viewZoneChangeAccessor.addZone(this._viewZone); - this._viewZoneId = viewZoneChangeAccessor.addZone(this._viewZone); - this._editor.addContentWidget(this._contentWidget); + if (lenses.length > 0) { + this._createContentWidgetIfNecessary(); + this._contentWidget!.withCommands(lenses, false); + } + } + + private _createContentWidgetIfNecessary(): void { + if (!this._contentWidget) { + this._contentWidget = new CodeLensContentWidget(this._editor, this._className, this._viewZone.afterLineNumber + 1); + this._editor.addContentWidget(this._contentWidget!); } } dispose(helper: CodeLensHelper, viewZoneChangeAccessor?: editorBrowser.IViewZoneChangeAccessor): void { - while (this._decorationIds.length) { - helper.removeDecoration(this._decorationIds.pop()!); - } + this._decorationIds.forEach(helper.removeDecoration, helper); + this._decorationIds = []; if (viewZoneChangeAccessor) { viewZoneChangeAccessor.removeZone(this._viewZoneId); } - this._editor.removeContentWidget(this._contentWidget); + if (this._contentWidget) { + this._editor.removeContentWidget(this._contentWidget); + } } isValid(): boolean { - if (!this._editor.hasModel()) { - return false; - } - const model = this._editor.getModel(); return this._decorationIds.some((id, i) => { - const range = model.getDecorationRange(id); + const range = this._editor.getModel().getDecorationRange(id); const symbol = this._data[i].symbol; return !!(range && Range.isEmpty(symbol.range) === range.isEmpty()); }); } updateCodeLensSymbols(data: CodeLensItem[], helper: CodeLensHelper): void { - while (this._decorationIds.length) { - helper.removeDecoration(this._decorationIds.pop()!); - } + this._decorationIds.forEach(helper.removeDecoration, helper); + this._decorationIds = []; this._data = data; - this._decorationIds = new Array(this._data.length); this._data.forEach((codeLensData, i) => { helper.addDecoration({ range: codeLensData.symbol.range, @@ -275,7 +274,7 @@ export class CodeLensWidget { } computeIfNecessary(model: ITextModel): CodeLensItem[] | null { - if (!this._contentWidget.isVisible()) { + if (!this._viewZone.domNode.hasAttribute('monaco-visible-view-zone')) { return null; } @@ -290,7 +289,10 @@ export class CodeLensWidget { } updateCommands(symbols: Array): void { - this._contentWidget.withCommands(symbols, true); + + this._createContentWidgetIfNecessary(); + this._contentWidget!.withCommands(symbols, true); + for (let i = 0; i < this._data.length; i++) { const resolved = symbols[i]; if (resolved) { @@ -301,15 +303,13 @@ export class CodeLensWidget { } getCommand(link: HTMLLinkElement): Command | undefined { - return this._contentWidget.getCommand(link); + return this._contentWidget?.getCommand(link); } getLineNumber(): number { - if (this._editor.hasModel()) { - const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]); - if (range) { - return range.startLineNumber; - } + const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]); + if (range) { + return range.startLineNumber; } return -1; } @@ -321,8 +321,10 @@ export class CodeLensWidget { this._viewZone.afterLineNumber = range.startLineNumber - 1; viewZoneChangeAccessor.layoutZone(this._viewZoneId); - this._contentWidget.setSymbolRange(range); - this._editor.layoutContentWidget(this._contentWidget); + if (this._contentWidget) { + this._contentWidget.updatePosition(range.startLineNumber); + this._editor.layoutContentWidget(this._contentWidget); + } } } } diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 9d7b3fa0b19..33813997edb 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -25,12 +25,11 @@ import { AriaProvider, DataSource, Delegate, FileReferencesRenderer, OneReferenc import * as nls from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; -import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { WorkbenchAsyncDataTree, IWorkbenchAsyncDataTreeOptions } from 'vs/platform/list/browser/listService'; import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import * as peekView from 'vs/editor/contrib/peekView/peekView'; import { FileReferences, OneReference, ReferencesModel } from '../referencesModel'; -import { IAsyncDataTreeOptions } from 'vs/base/browser/ui/tree/asyncDataTree'; import { FuzzyScore } from 'vs/base/common/filters'; import { SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; @@ -227,7 +226,6 @@ export class ReferenceWidget extends peekView.PeekViewWidget { this.setModel(undefined); this._callOnDispose.dispose(); this._disposeOnNewModel.dispose(); - this._preview.setModel(null); // drop all view-zones, workaround for https://github.com/microsoft/vscode/issues/84726 dispose(this._preview); dispose(this._previewNotAvailableMessage); dispose(this._tree); @@ -298,12 +296,15 @@ export class ReferenceWidget extends peekView.PeekViewWidget { // tree this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline')); - const treeOptions: IAsyncDataTreeOptions = { + const treeOptions: IWorkbenchAsyncDataTreeOptions = { ariaLabel: nls.localize('treeAriaLabel', "References"), keyboardSupport: this._defaultTreeKeyboardSupport, accessibilityProvider: new AriaProvider(), keyboardNavigationLabelProvider: this._instantiationService.createInstance(StringRepresentationProvider), - identityProvider: new IdentityProvider() + identityProvider: new IdentityProvider(), + overrideStyles: { + listBackground: peekView.peekViewResultsBackground + } }; this._tree = this._instantiationService.createInstance>( WorkbenchAsyncDataTree, @@ -315,7 +316,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { this._instantiationService.createInstance(OneReferenceRenderer), ], this._instantiationService.createInstance(DataSource), - treeOptions + treeOptions, ); // split stuff diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index 39642a4d6e6..3dfa86a85a8 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -421,24 +421,22 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { public getLoadStatus(): ILoadStatus { let promises: Thenable[] = []; for (let nestedModeId in this._embeddedModes) { - if (this._embeddedModes.hasOwnProperty(nestedModeId)) { - const tokenizationSupport = modes.TokenizationRegistry.get(nestedModeId); - if (tokenizationSupport) { - // The nested mode is already loaded - if (tokenizationSupport instanceof MonarchTokenizer) { - const nestedModeStatus = tokenizationSupport.getLoadStatus(); - if (nestedModeStatus.loaded === false) { - promises.push(nestedModeStatus.promise); - } + const tokenizationSupport = modes.TokenizationRegistry.get(nestedModeId); + if (tokenizationSupport) { + // The nested mode is already loaded + if (tokenizationSupport instanceof MonarchTokenizer) { + const nestedModeStatus = tokenizationSupport.getLoadStatus(); + if (nestedModeStatus.loaded === false) { + promises.push(nestedModeStatus.promise); } - continue; } + continue; + } - const tokenizationSupportPromise = modes.TokenizationRegistry.getPromise(nestedModeId); - if (tokenizationSupportPromise) { - // The nested mode is in the process of being loaded - promises.push(tokenizationSupportPromise); - } + const tokenizationSupportPromise = modes.TokenizationRegistry.getPromise(nestedModeId); + if (tokenizationSupportPromise) { + // The nested mode is in the process of being loaded + promises.push(tokenizationSupportPromise); } } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 2fa74f0ac35..b1d411f7aef 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5560,6 +5560,32 @@ declare namespace monaco.languages { resolveCodeLens?(model: editor.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } + export interface SemanticColoringLegend { + readonly tokenTypes: string[]; + readonly tokenModifiers: string[]; + } + + export interface SemanticColoringArea { + /** + * The zero-based line value where this token block begins. + */ + readonly line: number; + /** + * The actual token block encoded data. + */ + readonly data: Uint32Array; + } + + export interface SemanticColoring { + readonly areas: SemanticColoringArea[]; + dispose(): void; + } + + export interface SemanticColoringProvider { + getLegend(): SemanticColoringLegend; + provideSemanticColoring(model: editor.ITextModel, token: CancellationToken): ProviderResult; + } + export interface ILanguageExtensionPoint { id: string; extensions?: string[]; diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 277c5f3d728..20235f709c7 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -22,7 +22,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; -import { attachListStyler, computeStyles, defaultListStyles } from 'vs/platform/theme/common/styler'; +import { attachListStyler, computeStyles, defaultListStyles, IColorMapping, attachStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { ObjectTree, IObjectTreeOptions, ICompressibleTreeRenderer, CompressibleObjectTree, ICompressibleObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; @@ -55,6 +55,7 @@ export class ListService implements IListService { _serviceBrand: undefined; + private disposables = new DisposableStore(); private lists: IRegisteredList[] = []; private _lastFocusedWidget: ListWidget | undefined = undefined; @@ -62,7 +63,11 @@ export class ListService implements IListService { return this._lastFocusedWidget; } - constructor(@IContextKeyService contextKeyService: IContextKeyService) { } + constructor(@IThemeService themeService: IThemeService) { + // create a shared default tree style sheet for performance reasons + const styleController = new DefaultStyleController(createStyleSheet(), ''); + this.disposables.add(attachListStyler(styleController, themeService)); + } register(widget: ListWidget, extraContextKeys?: (IContextKey)[]): IDisposable { if (this.lists.some(l => l.widget === widget)) { @@ -89,6 +94,10 @@ export class ListService implements IListService { }) ); } + + dispose(): void { + this.disposables.dispose(); + } } const RawWorkbenchListFocusContextKey = new RawContextKey('listFocus', true); @@ -221,13 +230,8 @@ function toWorkbenchListOptions(options: IListOptions, configurationServic return [result, disposables]; } -let sharedListStyleSheet: HTMLStyleElement; -function getSharedListStyleSheet(): HTMLStyleElement { - if (!sharedListStyleSheet) { - sharedListStyleSheet = createStyleSheet(); - } - - return sharedListStyleSheet; +export interface IWorkbenchListOptions extends IListOptions { + readonly overrideStyles?: IColorMapping; } export class WorkbenchList extends List { @@ -246,7 +250,7 @@ export class WorkbenchList extends List { container: HTMLElement, delegate: IListVirtualDelegate, renderers: IListRenderer[], - options: IListOptions, + options: IWorkbenchListOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @@ -259,7 +263,6 @@ export class WorkbenchList extends List { super(user, container, delegate, renderers, { keyboardSupport: false, - styleController: new DefaultStyleController(getSharedListStyleSheet()), ...computeStyles(themeService.getTheme(), defaultListStyles), ...workbenchListOptions, horizontalScrolling @@ -282,7 +285,11 @@ export class WorkbenchList extends List { this.disposables.add(this.contextKeyService); this.disposables.add((listService as ListService).register(this)); - this.disposables.add(attachListStyler(this, themeService)); + + if (options.overrideStyles) { + this.disposables.add(attachStyler(themeService, options.overrideStyles, this)); + } + this.disposables.add(this.onSelectionChange(() => { const selection = this.getSelection(); const focus = this.getFocus(); @@ -328,7 +335,7 @@ export class WorkbenchPagedList extends PagedList { container: HTMLElement, delegate: IListVirtualDelegate, renderers: IPagedRenderer[], - options: IListOptions, + options: IWorkbenchListOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @@ -340,7 +347,6 @@ export class WorkbenchPagedList extends PagedList { super(user, container, delegate, renderers, { keyboardSupport: false, - styleController: new DefaultStyleController(getSharedListStyleSheet()), ...computeStyles(themeService.getTheme(), defaultListStyles), ...workbenchListOptions, horizontalScrolling @@ -360,7 +366,10 @@ export class WorkbenchPagedList extends PagedList { this.disposables.add(this.contextKeyService); this.disposables.add((listService as ListService).register(this)); - this.disposables.add(attachListStyler(this, themeService)); + + if (options.overrideStyles) { + this.disposables.add(attachStyler(themeService, options.overrideStyles, this)); + } this.registerListeners(); } @@ -777,6 +786,10 @@ function createKeyboardNavigationEventFilter(container: HTMLElement, keybindingS }; } +export interface IWorkbenchObjectTreeOptions extends IObjectTreeOptions { + readonly overrideStyles?: IColorMapping; +} + export class WorkbenchObjectTree, TFilterData = void> extends ObjectTree { private internals: WorkbenchTreeInternals; @@ -788,7 +801,7 @@ export class WorkbenchObjectTree, TFilterData = void> container: HTMLElement, delegate: IListVirtualDelegate, renderers: ITreeRenderer[], - options: IObjectTreeOptions, + options: IWorkbenchObjectTreeOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @@ -796,14 +809,18 @@ export class WorkbenchObjectTree, TFilterData = void> @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, themeService, configurationService, keybindingService, accessibilityService); + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); super(user, container, delegate, renderers, treeOptions); this.disposables.add(disposable); - this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, contextKeyService, listService, themeService, configurationService, accessibilityService); + this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService); this.disposables.add(this.internals); } } +export interface IWorkbenchCompressibleObjectTreeOptions extends ICompressibleObjectTreeOptions { + readonly overrideStyles?: IColorMapping; +} + export class WorkbenchCompressibleObjectTree, TFilterData = void> extends CompressibleObjectTree { private internals: WorkbenchTreeInternals; @@ -815,7 +832,7 @@ export class WorkbenchCompressibleObjectTree, TFilter container: HTMLElement, delegate: IListVirtualDelegate, renderers: ICompressibleTreeRenderer[], - options: ICompressibleObjectTreeOptions, + options: IWorkbenchCompressibleObjectTreeOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @@ -823,14 +840,18 @@ export class WorkbenchCompressibleObjectTree, TFilter @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, themeService, configurationService, keybindingService, accessibilityService); + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); super(user, container, delegate, renderers, treeOptions); this.disposables.add(disposable); - this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, contextKeyService, listService, themeService, configurationService, accessibilityService); + this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService); this.disposables.add(this.internals); } } +export interface IWorkbenchDataTreeOptions extends IDataTreeOptions { + readonly overrideStyles?: IColorMapping; +} + export class WorkbenchDataTree extends DataTree { private internals: WorkbenchTreeInternals; @@ -843,7 +864,7 @@ export class WorkbenchDataTree extends DataTree, renderers: ITreeRenderer[], dataSource: IDataSource, - options: IDataTreeOptions, + options: IWorkbenchDataTreeOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @@ -851,14 +872,18 @@ export class WorkbenchDataTree extends DataTree extends IAsyncDataTreeOptions { + readonly overrideStyles?: IColorMapping; +} + export class WorkbenchAsyncDataTree extends AsyncDataTree { private internals: WorkbenchTreeInternals; @@ -871,7 +896,7 @@ export class WorkbenchAsyncDataTree extends Async delegate: IListVirtualDelegate, renderers: ITreeRenderer[], dataSource: IAsyncDataSource, - options: IAsyncDataTreeOptions, + options: IWorkbenchAsyncDataTreeOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @@ -879,14 +904,18 @@ export class WorkbenchAsyncDataTree extends Async @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, themeService, configurationService, keybindingService, accessibilityService); + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); super(user, container, delegate, renderers, dataSource, treeOptions); this.disposables.add(disposable); - this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, contextKeyService, listService, themeService, configurationService, accessibilityService); + this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService); this.disposables.add(this.internals); } } +export interface IWorkbenchCompressibleAsyncDataTreeOptions extends ICompressibleAsyncDataTreeOptions { + readonly overrideStyles?: IColorMapping; +} + export class WorkbenchCompressibleAsyncDataTree extends CompressibleAsyncDataTree { private internals: WorkbenchTreeInternals; @@ -900,7 +929,7 @@ export class WorkbenchCompressibleAsyncDataTree e compressionDelegate: ITreeCompressionDelegate, renderers: ICompressibleTreeRenderer[], dataSource: IAsyncDataSource, - options: ICompressibleAsyncDataTreeOptions, + options: IWorkbenchCompressibleAsyncDataTreeOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @@ -908,10 +937,10 @@ export class WorkbenchCompressibleAsyncDataTree e @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, themeService, configurationService, keybindingService, accessibilityService); + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, configurationService, keybindingService, accessibilityService); super(user, container, virtualDelegate, compressionDelegate, renderers, dataSource, treeOptions); this.disposables.add(disposable); - this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, contextKeyService, listService, themeService, configurationService, accessibilityService); + this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService); this.disposables.add(this.internals); } } @@ -920,7 +949,6 @@ function workbenchTreeDataPreamble(treeIndentKey), renderIndentGuides: configurationService.getValue(treeRenderIndentGuidesKey), @@ -986,6 +1012,7 @@ class WorkbenchTreeInternals { tree: WorkbenchObjectTree | CompressibleObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree, options: IAbstractTreeOptions | IAsyncDataTreeOptions, getAutomaticKeyboardNavigation: () => boolean | undefined, + overrideStyles: IColorMapping | undefined, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @@ -1017,7 +1044,7 @@ class WorkbenchTreeInternals { this.disposables.push( this.contextKeyService, (listService as ListService).register(tree), - attachListStyler(tree, themeService), + overrideStyles ? attachStyler(themeService, overrideStyles, tree) : Disposable.None, tree.onDidChangeSelection(() => { const selection = tree.getSelection(); const focus = tree.getFocus(); diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index 7a3decaa26e..6886eb727bb 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -7,7 +7,6 @@ import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, darken, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground, treeIndentGuidesStroke, editorWidgetForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, ColorValue, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; -import { mixin } from 'vs/base/common/objects'; import { IThemable, styleFn } from 'vs/base/common/styler'; export interface IStyleOverrides { @@ -204,6 +203,7 @@ export function attachQuickOpenStyler(widget: IThemable, themeService: IThemeSer } export interface IListStyleOverrides extends IStyleOverrides { + listBackground?: ColorIdentifier; listFocusBackground?: ColorIdentifier; listFocusForeground?: ColorIdentifier; listActiveSelectionBackground?: ColorIdentifier; @@ -227,8 +227,8 @@ export interface IListStyleOverrides extends IStyleOverrides { treeIndentGuidesStroke?: ColorIdentifier; } -export function attachListStyler(widget: IThemable, themeService: IThemeService, overrides?: IListStyleOverrides): IDisposable { - return attachStyler(themeService, mixin(overrides || Object.create(null), defaultListStyles, false) as IListStyleOverrides, widget); +export function attachListStyler(widget: IThemable, themeService: IThemeService, overrides?: IColorMapping): IDisposable { + return attachStyler(themeService, { ...defaultListStyles, ...(overrides || {}) }, widget); } export const defaultListStyles: IColorMapping = { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 51728ea8fb4..889ed495c15 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -68,6 +68,67 @@ declare module 'vscode' { //#endregion + //#region Alex - semantic coloring + + export class SemanticColoringLegend { + public readonly tokenTypes: string[]; + public readonly tokenModifiers: string[]; + + constructor(tokenTypes: string[], tokenModifiers: string[]); + } + + export class SemanticColoringArea { + /** + * The zero-based line value where this token block begins. + */ + public readonly line: number; + /** + * The actual token block encoded data. + * A certain token (at index `i` is encoded using 5 uint32 integers): + * - at index `5*i` - `deltaLine`: token line number, relative to `SemanticColoringArea.line` + * - at index `5*i+1` - `startCharacter`: token start character offset inside the line (inclusive) + * - at index `5*i+2` - `endCharacter`: token end character offset inside the line (exclusive) + * - at index `5*i+3` - `tokenType`: will be looked up in `SemanticColoringLegend.tokenTypes` + * - at index `5*i+4` - `tokenModifiers`: each set bit will be looked up in `SemanticColoringLegend.tokenModifiers` + */ + public readonly data: Uint32Array; + + constructor(line: number, data: Uint32Array); + } + + export class SemanticColoring { + public readonly areas: SemanticColoringArea[]; + + constructor(areas: SemanticColoringArea[]); + } + + /** + * The semantic coloring provider interface defines the contract between extensions and + * semantic coloring. + * + * + */ + export interface SemanticColoringProvider { + + provideSemanticColoring(document: TextDocument, token: CancellationToken): ProviderResult; + } + + export namespace languages { + /** + * Register a semantic coloring provider. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and the best-matching provider is used. Failure + * of the selected provider will cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A semantic coloring provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerSemanticColoringProvider(selector: DocumentSelector, provider: SemanticColoringProvider, legend: SemanticColoringLegend): Disposable; + } + + //#endregion // #region Joh - code insets @@ -764,43 +825,71 @@ declare module 'vscode' { //#region mjbvz,joh: https://github.com/Microsoft/vscode/issues/43768 export interface FileCreateEvent { - readonly created: ReadonlyArray; + + /** + * The files that got created. + */ + readonly files: ReadonlyArray; } export interface FileWillCreateEvent { - readonly creating: ReadonlyArray; + + /** + * The files that are going to be created. + */ + readonly files: ReadonlyArray; + + waitUntil(thenable: Thenable): void; waitUntil(thenable: Thenable): void; } export interface FileDeleteEvent { - readonly deleted: ReadonlyArray; + + /** + * The files that got deleted. + */ + readonly files: ReadonlyArray; } export interface FileWillDeleteEvent { - readonly deleting: ReadonlyArray; + + /** + * The files that are going to be deleted. + */ + readonly files: ReadonlyArray; + + waitUntil(thenable: Thenable): void; waitUntil(thenable: Thenable): void; } export interface FileRenameEvent { - readonly renamed: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + + /** + * The files that got renamed. + */ + readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; } export interface FileWillRenameEvent { - readonly renaming: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; - waitUntil(thenable: Thenable): void; // TODO@joh support sync/async + + /** + * The files that are going to be renamed. + */ + readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + + waitUntil(thenable: Thenable): void; + waitUntil(thenable: Thenable): void; } export namespace workspace { export const onWillCreateFiles: Event; - export const onDidCreateFiles: Event; - export const onWillDeleteFiles: Event; - export const onDidDeleteFiles: Event; - export const onWillRenameFiles: Event; - export const onDidRenameFiles: Event; + export const onDidCreateFiles: Event; + export const onDidDeleteFiles: Event; + export const onDidRenameFiles: Event; } //#endregion diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index c00e6162bcb..8b3849988cc 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -21,6 +21,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import * as callh from 'vs/workbench/contrib/callHierarchy/browser/callHierarchy'; import { mixin } from 'vs/base/common/objects'; +import { decodeSemanticTokensDto, ISemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokens'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { @@ -324,6 +325,12 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha })); } + // --- semantic coloring + + $registerSemanticColoringProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticColoringLegend): void { + this._registrations.set(handle, modes.SemanticColoringProviderRegistry.register(selector, new MainThreadSemanticColoringProvider(this._proxy, handle, legend))); + } + // --- suggest private static _inflateSuggestDto(defaultRange: IRange | { insert: IRange, replace: IRange }, data: ISuggestDataDto): modes.CompletionItem { @@ -594,3 +601,76 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } } + +class MainThreadSemanticColoringCacheEntry implements modes.SemanticColoring { + + constructor( + private readonly _parent: MainThreadSemanticColoringProvider, + public readonly uri: URI, + public readonly id: number, + public readonly areas: modes.SemanticColoringArea[], + ) { + } + + dispose(): void { + this._parent.release(this); + } +} + +export class MainThreadSemanticColoringProvider implements modes.SemanticColoringProvider { + + private readonly _cache = new Map(); + + constructor( + private readonly _proxy: ExtHostLanguageFeaturesShape, + private readonly _handle: number, + private readonly _legend: modes.SemanticColoringLegend, + ) { + } + + release(entry: MainThreadSemanticColoringCacheEntry): void { + const currentCacheEntry = this._cache.get(entry.uri.toString()) || null; + if (currentCacheEntry && currentCacheEntry.id === entry.id) { + this._cache.delete(entry.uri.toString()); + } + this._proxy.$releaseSemanticColoring(this._handle, entry.id); + } + + getLegend(): modes.SemanticColoringLegend { + return this._legend; + } + + async provideSemanticColoring(model: ITextModel, token: CancellationToken): Promise { + const lastResult = this._cache.get(model.uri.toString()) || null; + const encodedDto = await this._proxy.$provideSemanticColoring(this._handle, model.uri, lastResult ? lastResult.id : 0, token); + if (!encodedDto) { + return null; + } + if (token.isCancellationRequested) { + return null; + } + const dto = decodeSemanticTokensDto(encodedDto); + const res = this._resolveDeltas(model, lastResult, dto); + this._cache.set(model.uri.toString(), res); + return res; + } + + private _resolveDeltas(model: ITextModel, lastResult: MainThreadSemanticColoringCacheEntry | null, dto: ISemanticTokensDto): MainThreadSemanticColoringCacheEntry { + let areas: modes.SemanticColoringArea[] = []; + for (let i = 0, len = dto.areas.length; i < len; i++) { + const areaDto = dto.areas[i]; + if (areaDto.type === 'full') { + areas[i] = { + line: areaDto.line, + data: areaDto.data + }; + } else { + areas[i] = { + line: areaDto.line, + data: lastResult!.areas[areaDto.oldIndex].data + }; + } + } + return new MainThreadSemanticColoringCacheEntry(this, model.uri, dto.id, areas); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts index 9d0778cfd0d..023e413f8cc 100644 --- a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts @@ -31,7 +31,7 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { ISaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; -import { SaveReason } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { SaveReason } from 'vs/workbench/common/editor'; import { ExtHostContext, ExtHostDocumentSaveParticipantShape, IExtHostContext } from '../common/extHost.protocol'; export interface ICodeActionsOnSaveOptions { diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 06138112961..691cd725136 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -37,7 +37,6 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { createCSSRule, asCSSUrl } from 'vs/base/browser/dom'; export interface IUserFriendlyViewsContainerDescriptor { id: string; @@ -254,10 +253,9 @@ class ViewsExtensionHandler implements IWorkbenchContribution { private registerTestViewContainer(): void { const title = localize('test', "Test"); - const cssClass = `extensionViewlet-test`; const icon = URI.parse(require.toUrl('./media/test.svg')); - this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, cssClass, undefined); + this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, undefined); } private isValidViewsContainer(viewsContainersDescriptors: IUserFriendlyViewsContainerDescriptor[], collector: ExtensionMessageCollector): boolean { @@ -290,10 +288,9 @@ class ViewsExtensionHandler implements IWorkbenchContribution { private registerCustomViewContainers(containers: IUserFriendlyViewsContainerDescriptor[], extension: IExtensionDescription, order: number, existingViewContainers: ViewContainer[]): number { containers.forEach(descriptor => { - const cssClass = `extensionViewlet-${descriptor.id}`; const icon = resources.joinPath(extension.extensionLocation, descriptor.icon); const id = `workbench.view.extension.${descriptor.id}`; - const viewContainer = this.registerCustomViewContainer(id, descriptor.title, icon, order++, cssClass, extension.identifier); + const viewContainer = this.registerCustomViewContainer(id, descriptor.title, icon, order++, extension.identifier); // Move those views that belongs to this container if (existingViewContainers.length) { @@ -311,7 +308,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { return order; } - private registerCustomViewContainer(id: string, title: string, icon: URI, order: number, cssClass: string, extensionId: ExtensionIdentifier | undefined): ViewContainer { + private registerCustomViewContainer(id: string, title: string, icon: URI, order: number, extensionId: ExtensionIdentifier | undefined): ViewContainer { let viewContainer = this.viewContainersRegistry.get(id); if (!viewContainer) { @@ -339,7 +336,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { CustomViewlet, id, title, - cssClass, + undefined, order, icon ); @@ -363,15 +360,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { `View: Show ${title}`, localize('view', "View") ); - - // Generate CSS to show the icon in the activity bar - const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${cssClass}`; - createCSSRule(iconClass, ` - mask: ${asCSSUrl(icon)} no-repeat 50% 50%; - mask-size: 24px; - -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; - -webkit-mask-size: 24px;` - ); } return viewContainer; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e71a93d69e4..93d36038334 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -351,6 +351,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerOnTypeFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.OnTypeFormattingEditProvider, firstTriggerCharacter: string, ...moreTriggerCharacters: string[]): vscode.Disposable { return extHostLanguageFeatures.registerOnTypeFormattingEditProvider(extension, checkSelector(selector), provider, [firstTriggerCharacter].concat(moreTriggerCharacters)); }, + registerSemanticColoringProvider(selector: vscode.DocumentSelector, provider: vscode.SemanticColoringProvider, legend: vscode.SemanticColoringLegend): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerSemanticColoringProvider(extension, checkSelector(selector), provider, legend); + }, registerSignatureHelpProvider(selector: vscode.DocumentSelector, provider: vscode.SignatureHelpProvider, firstItem?: string | vscode.SignatureHelpProviderMetadata, ...remaining: string[]): vscode.Disposable { if (typeof firstItem === 'object') { return extHostLanguageFeatures.registerSignatureHelpProvider(extension, checkSelector(selector), provider, firstItem); @@ -889,6 +893,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I RelativePattern: extHostTypes.RelativePattern, ResolvedAuthority: extHostTypes.ResolvedAuthority, RemoteAuthorityResolverError: extHostTypes.RemoteAuthorityResolverError, + SemanticColoring: extHostTypes.SemanticColoring, + SemanticColoringArea: extHostTypes.SemanticColoringArea, + SemanticColoringLegend: extHostTypes.SemanticColoringLegend, Selection: extHostTypes.Selection, SelectionRange: extHostTypes.SelectionRange, ShellExecution: extHostTypes.ShellExecution, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 88d442643ff..87349faf1cb 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -45,7 +45,7 @@ import { ITerminalDimensions, IShellLaunchConfig } from 'vs/workbench/contrib/te import { ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import * as search from 'vs/workbench/services/search/common/search'; -import { SaveReason } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { SaveReason } from 'vs/workbench/common/editor'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; export interface IEnvironment { @@ -354,6 +354,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void; $registerNavigateTypeSupport(handle: number): void; $registerRenameSupport(handle: number, selector: IDocumentFilterDto[], supportsResolveInitialValues: boolean): void; + $registerSemanticColoringProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticColoringLegend): void; $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, extensionId: ExtensionIdentifier): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void; @@ -1158,6 +1159,8 @@ export interface ExtHostLanguageFeaturesShape { $releaseWorkspaceSymbols(handle: number, id: number): void; $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string, token: CancellationToken): Promise; $resolveRenameLocation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; + $provideSemanticColoring(handle: number, resource: UriComponents, previousSemanticColoringResultId: number, token: CancellationToken): Promise; + $releaseSemanticColoring(handle: number, semanticColoringResultId: number): void; $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.CompletionContext, token: CancellationToken): Promise; $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; diff --git a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts index 3b1b80c1de4..63f885cc69a 100644 --- a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts @@ -11,7 +11,7 @@ import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, IResou import { TextEdit } from 'vs/workbench/api/common/extHostTypes'; import { Range, TextDocumentSaveReason, EndOfLine } from 'vs/workbench/api/common/extHostTypeConverters'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { SaveReason } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { SaveReason } from 'vs/workbench/common/editor'; import * as vscode from 'vscode'; import { LinkedList } from 'vs/base/common/linkedList'; import { ILogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index a290fb0cc0f..f39a22d203e 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten } from 'vs/base/common/arrays'; import { AsyncEmitter, Emitter, Event, IWaitUntil } from 'vs/base/common/event'; import { IRelativePattern, parse } from 'vs/base/common/glob'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import * as vscode from 'vscode'; -import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, IResourceFileEditDto, IResourceTextEditDto, MainThreadTextEditorsShape } from './extHost.protocol'; +import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, IResourceFileEditDto, IResourceTextEditDto } from './extHost.protocol'; import * as typeConverter from './extHostTypeConverters'; import { Disposable, WorkspaceEdit } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { FileOperation } from 'vs/platform/files/common/files'; +import { flatten } from 'vs/base/common/arrays'; class FileSystemWatcher implements vscode.FileSystemWatcher { @@ -142,13 +142,13 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ $onDidRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined): void { switch (operation) { case FileOperation.MOVE: - this._onDidRenameFile.fire(Object.freeze({ renamed: [{ oldUri: URI.revive(source!), newUri: URI.revive(target) }] })); + this._onDidRenameFile.fire(Object.freeze({ files: [{ oldUri: URI.revive(source!), newUri: URI.revive(target) }] })); break; case FileOperation.DELETE: - this._onDidDeleteFile.fire(Object.freeze({ deleted: [URI.revive(target)] })); + this._onDidDeleteFile.fire(Object.freeze({ files: [URI.revive(target)] })); break; case FileOperation.CREATE: - this._onDidCreateFile.fire(Object.freeze({ created: [URI.revive(target)] })); + this._onDidCreateFile.fire(Object.freeze({ files: [URI.revive(target)] })); break; default: //ignore, dont send @@ -179,38 +179,39 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ async $onWillRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined): Promise { switch (operation) { case FileOperation.MOVE: - await this._fireWillRename(URI.revive(source!), URI.revive(target)); + await this._fireWillEvent(this._onWillRenameFile, { files: [{ oldUri: URI.revive(source!), newUri: URI.revive(target) }], }); break; case FileOperation.DELETE: - this._onWillDeleteFile.fireAsync(thenables => ({ deleting: [URI.revive(target)], waitUntil: p => thenables.push(Promise.resolve(p)) })); + await this._fireWillEvent(this._onWillDeleteFile, { files: [URI.revive(target)] }); break; case FileOperation.CREATE: - this._onWillCreateFile.fireAsync(thenables => ({ creating: [URI.revive(target)], waitUntil: p => thenables.push(Promise.resolve(p)) })); + await this._fireWillEvent(this._onWillCreateFile, { files: [URI.revive(target)] }); break; default: //ignore, dont send } } - private async _fireWillRename(oldUri: URI, newUri: URI): Promise { + private async _fireWillEvent(emitter: AsyncEmitter, data: Omit): Promise { const edits: WorkspaceEdit[] = []; - await Promise.resolve(this._onWillRenameFile.fireAsync(bucket => { - return { - renaming: [{ oldUri, newUri }], - waitUntil: (thenable: Promise): void => { - if (Object.isFrozen(bucket)) { - throw new TypeError('waitUntil cannot be called async'); - } - const index = bucket.length; - const wrappedThenable = Promise.resolve(thenable).then(result => { - // ignore all results except for WorkspaceEdits. Those - // are stored in a spare array - if (result instanceof WorkspaceEdit) { - edits[index] = result; + await Promise.resolve(emitter.fireAsync(bucket => { + return { + ...data, + ...{ + waitUntil: (thenable: Promise): void => { + if (Object.isFrozen(bucket)) { + throw new TypeError('waitUntil cannot be called async'); } - }); - bucket.push(wrappedThenable); + const promise = Promise.resolve(thenable).then(result => { + // ignore all results except for WorkspaceEdits. Those + // are stored in a spare array + if (result instanceof WorkspaceEdit) { + edits.push(result); + } + }); + bucket.push(promise); + } } }; })); @@ -223,13 +224,9 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ // and apply them in one go. const allEdits = new Array>(); for (let edit of edits) { - if (edit) { // sparse array - let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); - allEdits.push(edits); - } + let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); + allEdits.push(edits); } return this._mainThreadTextEditors.$tryApplyWorkspaceEdit({ edits: flatten(allEdits) }); } - - } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index fb15773114e..30c3818861f 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -7,7 +7,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { mixin } from 'vs/base/common/objects'; import * as vscode from 'vscode'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol } from 'vs/workbench/api/common/extHostTypes'; +import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticColoringArea } from 'vs/workbench/api/common/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -26,6 +26,8 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IURITransformer } from 'vs/base/common/uriIpc'; import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { encodeSemanticTokensDto, ISemanticTokensDto, ISemanticTokensAreaDto } from 'vs/workbench/api/common/shared/semanticTokens'; import { IdGenerator } from 'vs/base/common/idGenerator'; // --- adapter @@ -613,6 +615,364 @@ class RenameAdapter { } } +export const enum SemanticColoringConstants { + /** + * Let's aim at having 8KB buffers if possible... + * So that would be 8192 / (5 * 4) = 409.6 tokens per area + */ + DesiredTokensPerArea = 400, + + /** + * Try to keep the total number of areas under 1024 if possible, + * simply compensate by having more tokens per area... + */ + DesiredMaxAreas = 1024, + + /** + * Threshold for merging multiple delta areas and sending a full area. + */ + MinTokensPerArea = 50 +} + +interface ISemanticColoringAreaPair { + data: Uint32Array; + dto: ISemanticTokensAreaDto; +} + +export class SemanticColoringAdapter { + + private readonly _previousResults: Map; + private readonly _splitSingleAreaTokenCountThreshold: number; + private _nextResultId = 1; + + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.SemanticColoringProvider, + private readonly _desiredTokensPerArea = SemanticColoringConstants.DesiredTokensPerArea, + private readonly _desiredMaxAreas = SemanticColoringConstants.DesiredMaxAreas, + private readonly _minTokensPerArea = SemanticColoringConstants.MinTokensPerArea + ) { + this._previousResults = new Map(); + this._splitSingleAreaTokenCountThreshold = Math.round(1.5 * this._desiredTokensPerArea); + } + + provideSemanticColoring(resource: URI, previousSemanticColoringResultId: number, token: CancellationToken): Promise { + const doc = this._documents.getDocument(resource); + + return asPromise(() => this._provider.provideSemanticColoring(doc, token)).then(value => { + if (!value) { + return null; + } + + const oldAreas = (previousSemanticColoringResultId !== 0 ? this._previousResults.get(previousSemanticColoringResultId) : null); + if (oldAreas) { + this._previousResults.delete(previousSemanticColoringResultId); + return this._deltaEncodeAreas(oldAreas, value.areas); + } + + return this._fullEncodeAreas(value.areas); + }); + } + + async releaseSemanticColoring(semanticColoringResultId: number): Promise { + this._previousResults.delete(semanticColoringResultId); + } + + private _deltaEncodeAreas(oldAreas: Uint32Array[], newAreas: SemanticColoringArea[]): VSBuffer { + if (newAreas.length > 1) { + // this is a fancy provider which is smart enough to break things into good areas + // we therefore try to match old areas only by object identity + const oldAreasIndexMap = new Map(); + for (let i = 0, len = oldAreas.length; i < len; i++) { + oldAreasIndexMap.set(oldAreas[i], i); + } + + let result: ISemanticColoringAreaPair[] = []; + for (let i = 0, len = newAreas.length; i < len; i++) { + const newArea = newAreas[i]; + if (oldAreasIndexMap.has(newArea.data)) { + // great! we can reuse this area + const oldIndex = oldAreasIndexMap.get(newArea.data)!; + result.push({ + data: newArea.data, + dto: { + type: 'delta', + line: newArea.line, + oldIndex: oldIndex + } + }); + } else { + result.push({ + data: newArea.data, + dto: { + type: 'full', + line: newArea.line, + data: newArea.data + } + }); + } + } + + return this._saveResultAndEncode(result); + } + + return this._deltaEncodeArea(oldAreas, newAreas[0]); + } + + private static _oldAreaAppearsInNewArea(oldAreaData: Uint32Array, oldAreaTokenCount: number, newAreaData: Uint32Array, newAreaOffset: number): boolean { + const newTokenStartDeltaLine = newAreaData[5 * newAreaOffset]; + + // check that each and every value from `oldArea` is equal to `area` + for (let j = 0; j < oldAreaTokenCount; j++) { + const oldOffset = 5 * j; + const newOffset = 5 * (j + newAreaOffset); + + if ( + (oldAreaData[oldOffset] !== newAreaData[newOffset] - newTokenStartDeltaLine) + || (oldAreaData[oldOffset + 1] !== newAreaData[newOffset + 1]) + || (oldAreaData[oldOffset + 2] !== newAreaData[newOffset + 2]) + || (oldAreaData[oldOffset + 3] !== newAreaData[newOffset + 3]) + || (oldAreaData[oldOffset + 4] !== newAreaData[newOffset + 4]) + ) { + return false; + } + } + + return true; + } + + private _deltaEncodeArea(oldAreas: Uint32Array[], newArea: SemanticColoringArea): VSBuffer { + const newAreaData = newArea.data; + const prependAreas: ISemanticColoringAreaPair[] = []; + const appendAreas: ISemanticColoringAreaPair[] = []; + + // Try to find appearences of `oldAreas` inside `area`. + let newTokenStartIndex = 0; + let newTokenEndIndex = (newAreaData.length / 5) | 0; + let oldAreaUsedIndex = -1; + for (let i = 0, len = oldAreas.length; i < len; i++) { + const oldAreaData = oldAreas[i]; + const oldAreaTokenCount = (oldAreaData.length / 5) | 0; + if (oldAreaTokenCount === 0) { + // skip old empty areas + continue; + } + if (newTokenEndIndex - newTokenStartIndex < oldAreaTokenCount) { + // there are too many old tokens, this cannot work + break; + } + + const newAreaOffset = newTokenStartIndex; + const newTokenStartDeltaLine = newAreaData[5 * newAreaOffset]; + const isEqual = SemanticColoringAdapter._oldAreaAppearsInNewArea(oldAreaData, oldAreaTokenCount, newAreaData, newAreaOffset); + if (!isEqual) { + break; + } + newTokenStartIndex += oldAreaTokenCount; + + oldAreaUsedIndex = i; + prependAreas.push({ + data: oldAreaData, + dto: { + type: 'delta', + line: newArea.line + newTokenStartDeltaLine, + oldIndex: i + } + }); + } + + for (let i = oldAreas.length - 1; i > oldAreaUsedIndex; i--) { + const oldAreaData = oldAreas[i]; + const oldAreaTokenCount = (oldAreaData.length / 5) | 0; + if (oldAreaTokenCount === 0) { + // skip old empty areas + continue; + } + if (newTokenEndIndex - newTokenStartIndex < oldAreaTokenCount) { + // there are too many old tokens, this cannot work + break; + } + + const newAreaOffset = (newTokenEndIndex - oldAreaTokenCount); + const newTokenStartDeltaLine = newAreaData[5 * newAreaOffset]; + const isEqual = SemanticColoringAdapter._oldAreaAppearsInNewArea(oldAreaData, oldAreaTokenCount, newAreaData, newAreaOffset); + if (!isEqual) { + break; + } + newTokenEndIndex -= oldAreaTokenCount; + + appendAreas.unshift({ + data: oldAreaData, + dto: { + type: 'delta', + line: newArea.line + newTokenStartDeltaLine, + oldIndex: i + } + }); + } + + if (prependAreas.length === 0 && appendAreas.length === 0) { + // There is no reuse possibility! + return this._fullEncodeAreas([newArea]); + } + + if (newTokenStartIndex === newTokenEndIndex) { + // 100% reuse! + return this._saveResultAndEncode(prependAreas.concat(appendAreas)); + } + + // It is clear at this point that there will be at least one full area. + // Expand the mid area if the areas next to it are too small + while (prependAreas.length > 0) { + const tokenCount = (prependAreas[prependAreas.length - 1].data.length / 5); + if (tokenCount < this._minTokensPerArea) { + newTokenStartIndex -= tokenCount; + prependAreas.pop(); + } else { + break; + } + } + while (appendAreas.length > 0) { + const tokenCount = (appendAreas[0].data.length / 5); + if (tokenCount < this._minTokensPerArea) { + newTokenEndIndex += tokenCount; + appendAreas.shift(); + } else { + break; + } + } + + // Extract the mid area + const newTokenStartDeltaLine = newAreaData[5 * newTokenStartIndex]; + const newMidAreaData = new Uint32Array(5 * (newTokenEndIndex - newTokenStartIndex)); + for (let tokenIndex = newTokenStartIndex; tokenIndex < newTokenEndIndex; tokenIndex++) { + const srcOffset = 5 * tokenIndex; + const deltaLine = newAreaData[srcOffset]; + const startCharacter = newAreaData[srcOffset + 1]; + const endCharacter = newAreaData[srcOffset + 2]; + const tokenType = newAreaData[srcOffset + 3]; + const tokenModifiers = newAreaData[srcOffset + 4]; + + const destOffset = 5 * (tokenIndex - newTokenStartIndex); + newMidAreaData[destOffset] = deltaLine - newTokenStartDeltaLine; + newMidAreaData[destOffset + 1] = startCharacter; + newMidAreaData[destOffset + 2] = endCharacter; + newMidAreaData[destOffset + 3] = tokenType; + newMidAreaData[destOffset + 4] = tokenModifiers; + } + + const newMidArea = new SemanticColoringArea(newArea.line + newTokenStartDeltaLine, newMidAreaData); + const newMidAreas = this._splitAreaIntoMultipleAreasIfNecessary(newMidArea); + const newMidAreasPairs: ISemanticColoringAreaPair[] = newMidAreas.map(a => { + return { + data: a.data, + dto: { + type: 'full', + line: a.line, + data: a.data, + } + }; + }); + + return this._saveResultAndEncode(prependAreas.concat(newMidAreasPairs).concat(appendAreas)); + } + + private _fullEncodeAreas(areas: SemanticColoringArea[]): VSBuffer { + if (areas.length === 1) { + areas = this._splitAreaIntoMultipleAreasIfNecessary(areas[0]); + } + + return this._saveResultAndEncode(areas.map(a => { + return { + data: a.data, + dto: { + type: 'full', + line: a.line, + data: a.data + } + }; + })); + } + + private _saveResultAndEncode(areas: ISemanticColoringAreaPair[]): VSBuffer { + const myId = this._nextResultId++; + this._previousResults.set(myId, areas.map(a => a.data)); + console.log(`_saveResultAndEncode: ${myId} --> ${areas.map(a => `${a.dto.line}-${a.dto.type}(${a.data.length / 5})`).join(', ')}`); + const dto: ISemanticTokensDto = { + id: myId, + areas: areas.map(a => a.dto) + }; + return encodeSemanticTokensDto(dto); + } + + private _splitAreaIntoMultipleAreasIfNecessary(area: vscode.SemanticColoringArea): SemanticColoringArea[] { + const srcAreaLine = area.line; + const srcAreaData = area.data; + const tokenCount = (srcAreaData.length / 5) | 0; + if (tokenCount <= this._splitSingleAreaTokenCountThreshold) { + return [area]; + } + + const tokensPerArea = Math.max(Math.ceil(tokenCount / this._desiredMaxAreas), this._desiredTokensPerArea); + + let result: SemanticColoringArea[] = []; + let tokenIndex = 0; + while (tokenIndex < tokenCount) { + const tokenStartIndex = tokenIndex; + let tokenEndIndex = Math.min(tokenStartIndex + tokensPerArea, tokenCount); + + // Keep tokens on the same line in the same area... + if (tokenEndIndex < tokenCount) { + let smallAvoidDeltaLine = srcAreaData[5 * tokenEndIndex]; + let smallTokenEndIndex = tokenEndIndex; + while (smallTokenEndIndex - 1 > tokenStartIndex && srcAreaData[5 * (smallTokenEndIndex - 1)] === smallAvoidDeltaLine) { + smallTokenEndIndex--; + } + + if (smallTokenEndIndex - 1 === tokenStartIndex) { + // there are so many tokens on this line that our area would be empty, we must now go right + let bigAvoidDeltaLine = srcAreaData[5 * (tokenEndIndex - 1)]; + let bigTokenEndIndex = tokenEndIndex; + while (bigTokenEndIndex + 1 < tokenCount && srcAreaData[5 * (bigTokenEndIndex + 1)] === bigAvoidDeltaLine) { + bigTokenEndIndex++; + } + tokenEndIndex = bigTokenEndIndex; + } else { + tokenEndIndex = smallTokenEndIndex; + } + } + + let destAreaLine = 0; + const destAreaData = new Uint32Array((tokenEndIndex - tokenStartIndex) * 5); + while (tokenIndex < tokenEndIndex) { + const srcOffset = 5 * tokenIndex; + const line = srcAreaLine + srcAreaData[srcOffset]; + const startCharacter = srcAreaData[srcOffset + 1]; + const endCharacter = srcAreaData[srcOffset + 2]; + const tokenType = srcAreaData[srcOffset + 3]; + const tokenModifiers = srcAreaData[srcOffset + 4]; + + if (tokenIndex === tokenStartIndex) { + destAreaLine = line; + } + + const destOffset = 5 * (tokenIndex - tokenStartIndex); + destAreaData[destOffset] = line - destAreaLine; + destAreaData[destOffset + 1] = startCharacter; + destAreaData[destOffset + 2] = endCharacter; + destAreaData[destOffset + 3] = tokenType; + destAreaData[destOffset + 4] = tokenModifiers; + + tokenIndex++; + } + + result.push(new SemanticColoringArea(destAreaLine, destAreaData)); + } + + return result; + } +} + class SuggestAdapter { static supportsResolving(provider: vscode.CompletionItemProvider): boolean { @@ -1120,8 +1480,9 @@ class CallHierarchyAdapter { type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter - | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter - | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter; + | SemanticColoringAdapter | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter + | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter + | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter; class AdapterData { constructor( @@ -1445,6 +1806,24 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, RenameAdapter, adapter => adapter.resolveRenameLocation(URI.revive(resource), position, token), undefined); } + //#region semantic coloring + + registerSemanticColoringProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.SemanticColoringProvider, legend: vscode.SemanticColoringLegend): vscode.Disposable { + const handle = this._addNewAdapter(new SemanticColoringAdapter(this._documents, provider), extension); + this._proxy.$registerSemanticColoringProvider(handle, this._transformDocumentSelector(selector), legend); + return this._createDisposable(handle); + } + + $provideSemanticColoring(handle: number, resource: UriComponents, previousSemanticColoringResultId: number, token: CancellationToken): Promise { + return this._withAdapter(handle, SemanticColoringAdapter, adapter => adapter.provideSemanticColoring(URI.revive(resource), previousSemanticColoringResultId, token), null); + } + + $releaseSemanticColoring(handle: number, semanticColoringResultId: number): void { + this._withAdapter(handle, SemanticColoringAdapter, adapter => adapter.releaseSemanticColoring(semanticColoringResultId), undefined); + } + + //#endregion + // --- suggestion registerCompletionItemProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, triggerCharacters: string[]): vscode.Disposable { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 3361a6e5d66..08cfac73625 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -13,7 +13,7 @@ import { EndOfLineSequence, TrackedRangeStickiness } from 'vs/editor/common/mode import * as vscode from 'vscode'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ProgressLocation as MainProgressLocation } from 'vs/platform/progress/common/progress'; -import { SaveReason } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { SaveReason } from 'vs/workbench/common/editor'; import { IPosition } from 'vs/editor/common/core/position'; import * as editorRange from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index e49a9190983..bc50900c901 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2368,6 +2368,38 @@ export enum CommentMode { //#endregion +//#region Semantic Coloring + +export class SemanticColoringLegend { + public readonly tokenTypes: string[]; + public readonly tokenModifiers: string[]; + + constructor(tokenTypes: string[], tokenModifiers: string[]) { + this.tokenTypes = tokenTypes; + this.tokenModifiers = tokenModifiers; + } +} + +export class SemanticColoringArea { + public readonly line: number; + public readonly data: Uint32Array; + + constructor(line: number, data: Uint32Array) { + this.line = line; + this.data = data; + } +} + +export class SemanticColoring { + public readonly areas: SemanticColoringArea[]; + + constructor(areas: SemanticColoringArea[]) { + this.areas = areas; + } +} + +//#endregion + //#region debug export enum DebugConsoleMode { /** diff --git a/src/vs/workbench/api/common/shared/semanticTokens.ts b/src/vs/workbench/api/common/shared/semanticTokens.ts new file mode 100644 index 00000000000..0d7073ca65d --- /dev/null +++ b/src/vs/workbench/api/common/shared/semanticTokens.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; + +export interface ISemanticTokensFullAreaDto { + type: 'full'; + line: number; + data: Uint32Array; +} + +export interface ISemanticTokensDeltaAreaDto { + type: 'delta'; + line: number; + oldIndex: number; +} + +export type ISemanticTokensAreaDto = ISemanticTokensFullAreaDto | ISemanticTokensDeltaAreaDto; + +export interface ISemanticTokensDto { + id: number; + areas: ISemanticTokensAreaDto[]; +} + +const enum EncodedSemanticTokensAreaType { + Full = 1, + Delta = 2 +} + +export function encodeSemanticTokensDto(semanticTokens: ISemanticTokensDto): VSBuffer { + const buff = VSBuffer.alloc(encodedSize(semanticTokens)); + let offset = 0; + buff.writeUInt32BE(semanticTokens.id, offset); offset += 4; + buff.writeUInt32BE(semanticTokens.areas.length, offset); offset += 4; + for (let i = 0; i < semanticTokens.areas.length; i++) { + offset = encodeArea(semanticTokens.areas[i], buff, offset); + } + return buff; +} + +function encodedSize(semanticTokens: ISemanticTokensDto): number { + let result = 0; + result += 4; // etag + result += 4; // area count + for (let i = 0; i < semanticTokens.areas.length; i++) { + result += encodedAreaSize(semanticTokens.areas[i]); + } + return result; +} + +export function decodeSemanticTokensDto(buff: VSBuffer): ISemanticTokensDto { + let offset = 0; + const id = buff.readUInt32BE(offset); offset += 4; + const areasCount = buff.readUInt32BE(offset); offset += 4; + let areas: ISemanticTokensAreaDto[] = []; + for (let i = 0; i < areasCount; i++) { + offset = decodeArea(buff, offset, areas); + } + return { + id: id, + areas: areas + }; +} + +function encodeArea(area: ISemanticTokensAreaDto, buff: VSBuffer, offset: number): number { + buff.writeUInt8(area.type === 'full' ? EncodedSemanticTokensAreaType.Full : EncodedSemanticTokensAreaType.Delta, offset); offset += 1; + buff.writeUInt32BE(area.line + 1, offset); offset += 4; + if (area.type === 'full') { + const tokens = area.data; + const tokenCount = (tokens.length / 5) | 0; + buff.writeUInt32BE(tokenCount, offset); offset += 4; + // here we are explicitly iterating an writing the ints again to ensure writing the desired endianness. + for (let i = 0; i < tokenCount; i++) { + const tokenOffset = 5 * i; + buff.writeUInt32BE(tokens[tokenOffset], offset); offset += 4; + buff.writeUInt32BE(tokens[tokenOffset + 1], offset); offset += 4; + buff.writeUInt32BE(tokens[tokenOffset + 2], offset); offset += 4; + buff.writeUInt32BE(tokens[tokenOffset + 3], offset); offset += 4; + buff.writeUInt32BE(tokens[tokenOffset + 4], offset); offset += 4; + } + // buff.set(VSBuffer.wrap(uint8), offset); offset += area.data.byteLength; + } else { + buff.writeUInt32BE(area.oldIndex, offset); offset += 4; + } + return offset; +} + +function encodedAreaSize(area: ISemanticTokensAreaDto): number { + let result = 0; + result += 1; // type + result += 4; // line + if (area.type === 'full') { + const tokens = area.data; + const tokenCount = (tokens.length / 5) | 0; + result += 4; // token count + result += tokenCount * 5 * 4; + return result; + } else { + result += 4; // old index + return result; + } +} + +function decodeArea(buff: VSBuffer, offset: number, areas: ISemanticTokensAreaDto[]): number { + const type: EncodedSemanticTokensAreaType = buff.readUInt8(offset); offset += 1; + const line = buff.readUInt32BE(offset); offset += 4; + if (type === EncodedSemanticTokensAreaType.Full) { + // here we are explicitly iterating and reading the ints again to ensure reading the desired endianness. + const tokenCount = buff.readUInt32BE(offset); offset += 4; + const data = new Uint32Array(5 * tokenCount); + for (let i = 0; i < tokenCount; i++) { + const destOffset = 5 * i; + data[destOffset] = buff.readUInt32BE(offset); offset += 4; + data[destOffset + 1] = buff.readUInt32BE(offset); offset += 4; + data[destOffset + 2] = buff.readUInt32BE(offset); offset += 4; + data[destOffset + 3] = buff.readUInt32BE(offset); offset += 4; + data[destOffset + 4] = buff.readUInt32BE(offset); offset += 4; + } + areas.push({ + type: 'full', + line: line, + data: data + }); + return offset; + } else { + const oldIndex = buff.readUInt32BE(offset); offset += 4; + areas.push({ + type: 'delta', + line: line, + oldIndex: oldIndex + }); + return offset; + } +} diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index fdcf9b346d1..45da3780a68 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -33,15 +33,45 @@ export class ViewletActivityAction extends ActivityAction { private static readonly preventDoubleClickDelay = 300; - private lastRun: number = 0; + private readonly viewletService: IViewletService; + private readonly layoutService: IWorkbenchLayoutService; + private readonly telemetryService: ITelemetryService; + + private lastRun: number; constructor( activity: IActivity, - @IViewletService private readonly viewletService: IViewletService, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @IViewletService viewletService: IViewletService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @ITelemetryService telemetryService: ITelemetryService ) { + ViewletActivityAction.generateIconCSS(activity); super(activity); + + this.lastRun = 0; + this.viewletService = viewletService; + this.layoutService = layoutService; + this.telemetryService = telemetryService; + } + + private static generateIconCSS(activity: IActivity): void { + if (activity.iconUrl) { + activity.cssClass = activity.cssClass || `activity-${activity.id.replace(/\./g, '-')}`; + const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${activity.cssClass}`; + DOM.createCSSRule(iconClass, ` + mask: ${DOM.asCSSUrl(activity.iconUrl)} no-repeat 50% 50%; + mask-size: 24px; + -webkit-mask: ${DOM.asCSSUrl(activity.iconUrl)} no-repeat 50% 50%; + -webkit-mask-size: 24px; + `); + } + } + + setActivity(activity: IActivity): void { + if (activity.iconUrl && this.activity.cssClass !== activity.cssClass) { + ViewletActivityAction.generateIconCSS(activity); + } + this.activity = activity; } async run(event: any): Promise { @@ -170,21 +200,7 @@ export class PlaceHolderViewletActivityAction extends ViewletActivityAction { @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @ITelemetryService telemetryService: ITelemetryService ) { - super({ id, name: id, cssClass: `extensionViewlet-placeholder-${id.replace(/\./g, '-')}` }, viewletService, layoutService, telemetryService); - - if (iconUrl) { - const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${this.class}`; // Generate Placeholder CSS to show the icon in the activity bar - DOM.createCSSRule(iconClass, ` - mask: ${DOM.asCSSUrl(iconUrl)} no-repeat 50% 50%; - mask-size: 24px; - -webkit-mask: ${DOM.asCSSUrl(iconUrl)} no-repeat 50% 50%; - -webkit-mask-size: 24px; - `); - } - } - - setActivity(activity: IActivity): void { - this.activity = activity; + super({ id, name: id, iconUrl }, viewletService, layoutService, telemetryService); } } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index cfcbbf171b1..0f86e922cc3 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -158,15 +158,12 @@ export class ActivityActionViewItem extends BaseActionViewItem { if (this.label) { if (this.options.icon) { const foreground = this._action.checked ? colors.activeBackgroundColor || colors.activeForegroundColor : colors.inactiveBackgroundColor || colors.inactiveForegroundColor; - // TODO @misolori find a cleaner way to do this - const isExtension = this.activity.cssClass?.indexOf('extensionViewlet') === 0; - if (!isExtension) { - // Apply foreground color to activity bar items (codicons) - this.label.style.color = foreground ? foreground.toString() : ''; - } else { - // Apply background color to extensions + remote explorer (svgs) - + if (this.activity.iconUrl) { + // Apply background color to activity bar item provided with iconUrls this.label.style.backgroundColor = foreground ? foreground.toString() : ''; + } else { + // Apply foreground color to activity bar items provided with codicons + this.label.style.color = foreground ? foreground.toString() : ''; } } else { const foreground = this._action.checked ? colors.activeForegroundColor : colors.inactiveForegroundColor; @@ -242,6 +239,7 @@ export class ActivityActionViewItem extends BaseActionViewItem { this.updateLabel(); this.updateTitle(this.activity.name); this.updateBadge(); + this.updateStyles(); } protected updateBadge(): void { @@ -319,15 +317,14 @@ export class ActivityActionViewItem extends BaseActionViewItem { this.label.className = 'action-label'; if (this.activity.cssClass) { - // TODO @misolori find a cleaner way to do this - const isExtension = this.activity.cssClass?.indexOf('extensionViewlet') === 0; - if (this.options.icon && !isExtension) { - // Only apply icon class to activity bar items (exclude extensions + remote explorer) - dom.addClass(this.label, 'codicon'); - } dom.addClass(this.label, this.activity.cssClass); } + if (this.options.icon && !this.activity.iconUrl) { + // Only apply codicon class to activity bar icon items without iconUrl + dom.addClass(this.label, 'codicon'); + } + if (!this.options.icon) { this.label.textContent = this.getAction().label; } @@ -496,11 +493,7 @@ export class CompositeActionViewItem extends ActivityActionViewItem { activityName = this.compositeActivityAction.activity.name; } - this.compositeActivity = { - id: this.compositeActivityAction.activity.id, - cssClass: this.compositeActivityAction.activity.cssClass, - name: activityName - }; + this.compositeActivity = { ...this.compositeActivityAction.activity, ... { name: activityName } }; } return this.compositeActivity; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index 0541cf5d157..4112f90a567 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -379,7 +379,10 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { sorter: new FileSorter(), filter: this._instantiationService.createInstance(FileFilter), identityProvider: new FileIdentityProvider(), - keyboardNavigationLabelProvider: new FileNavigationLabelProvider() + keyboardNavigationLabelProvider: new FileNavigationLabelProvider(), + overrideStyles: { + listBackground: breadcrumbsPickerBackground + } }); } diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 24e6f4bc58c..8dc609269f7 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { mixin } from 'vs/base/common/objects'; -import { IEditorInput, EditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection } from 'vs/workbench/common/editor'; +import { IEditorInput, EditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection, SaveReason } from 'vs/workbench/common/editor'; import { QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { EditorQuickOpenEntry, EditorQuickOpenEntryGroup, IEditorQuickOpenEntry, QuickOpenAction } from 'vs/workbench/browser/quickopen'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; @@ -646,10 +646,9 @@ export abstract class BaseCloseAllAction extends Action { let saveOrRevert: boolean; if (confirm === ConfirmResult.DONT_SAVE) { - await this.editorService.revertAll({ soft: true }); - saveOrRevert = true; + saveOrRevert = await this.editorService.revertAll({ soft: true }); } else { - saveOrRevert = await this.editorService.saveAll({ includeUntitled: true }); + saveOrRevert = await this.editorService.saveAll({ reason: SaveReason.EXPLICIT, includeUntitled: true }); } if (saveOrRevert) { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 87fdf2a8415..0b0fcb36fc7 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/editorgroupview'; import { EditorGroup, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup'; -import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditor, EditorGroupEditorsCountContext, toResource, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditor, EditorGroupEditorsCountContext, toResource, SideBySideEditor, SaveReason } from 'vs/workbench/common/editor'; import { Event, Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { addClass, addClasses, Dimension, trackFocus, toggleClass, removeClass, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor } from 'vs/base/browser/dom'; @@ -1310,7 +1310,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Otherwise, handle accordingly switch (res) { case ConfirmResult.SAVE: - const result = await editor.save(); + const result = await editor.save(this._group.id, { reason: SaveReason.EXPLICIT }); return !result; case ConfirmResult.DONT_SAVE: diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 8ba1dcd86e2..4dfe5dd897f 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -9,7 +9,7 @@ import { distinct, deepClone, assign } from 'vs/base/common/objects'; import { isObject, assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { Dimension } from 'vs/base/browser/dom'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { EditorInput, EditorOptions, IEditorMemento, ITextEditor } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorMemento, ITextEditor, SaveReason } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IEditorViewState, IEditor } from 'vs/editor/common/editorCommon'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -17,7 +17,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { SaveReason } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { isDiffEditor, isCodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index a0981c5c296..d4500e7dd6a 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -384,7 +384,7 @@ export class ContributableViewsModel extends Disposable { return 0; } - return (this.getViewOrder(a) - this.getViewOrder(b)) || this.getGroupOrderResult(a, b); + return (this.getViewOrder(a) - this.getViewOrder(b)) || this.getGroupOrderResult(a, b) || (a.id < b.id ? -1 : 1); } private getGroupOrderResult(a: IViewDescriptor, b: IViewDescriptor) { @@ -436,7 +436,7 @@ export class ContributableViewsModel extends Disposable { const splices = sortedDiff( this.viewDescriptors, viewDescriptors, - (a, b) => a.id === b.id ? 0 : a.id < b.id ? -1 : 1 + this.compareViewDescriptors.bind(this) ).reverse(); const toRemove: { index: number, viewDescriptor: IViewDescriptor; }[] = []; diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index f934a70b2dd..788ff74199f 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -419,16 +419,16 @@ export abstract class FilterViewContainerViewlet extends ViewContainerViewlet { } onDidAddViews(added: IAddedViewDescriptorRef[]): ViewletPanel[] { - // Check that allViews is ready - if (this.allViews.size === 0) { - this.updateAllViews(this.viewsModel.viewDescriptors); - } const panels: ViewletPanel[] = super.onDidAddViews(added); for (let i = 0; i < added.length; i++) { if (this.constantViewDescriptors.has(added[i].viewDescriptor.id)) { panels[i].setExpanded(false); } } + // Check that allViews is ready + if (this.allViews.size === 0) { + this.updateAllViews(this.viewsModel.viewDescriptors); + } return panels; } diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 5591737dd01..2e4c2f168e1 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -73,14 +73,10 @@ export class ViewletDescriptor extends CompositeDescriptor { name: string, cssClass?: string, order?: number, - private _iconUrl?: URI + readonly iconUrl?: URI ) { super(ctor, id, name, cssClass, order, id); } - - get iconUrl(): URI | undefined { - return this._iconUrl; - } } export const Extensions = { diff --git a/src/vs/workbench/common/activity.ts b/src/vs/workbench/common/activity.ts index 513894f2cea..79a7c6586a0 100644 --- a/src/vs/workbench/common/activity.ts +++ b/src/vs/workbench/common/activity.ts @@ -3,11 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { URI } from 'vs/base/common/uri'; + export interface IActivity { id: string; name: string; keybindingId?: string; cssClass?: string; + iconUrl?: URI; } -export const GLOBAL_ACTIVITY_ID = 'workbench.action.globalActivity'; \ No newline at end of file +export const GLOBAL_ACTIVITY_ID = 'workbench.action.globalActivity'; diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 938d57f515e..403dee7e209 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -20,7 +20,6 @@ import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { IPathData } from 'vs/platform/windows/common/windows'; import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; -import { ISaveOptions, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { isEqual } from 'vs/base/common/resources'; @@ -277,6 +276,68 @@ export const enum Verbosity { LONG } +export const enum SaveReason { + + /** + * Explicit user gesture. + */ + EXPLICIT = 1, + + /** + * Auto save after a timeout. + */ + AUTO = 2, + + /** + * Auto save after editor focus change. + */ + FOCUS_CHANGE = 3, + + /** + * Auto save after window change. + */ + WINDOW_CHANGE = 4 +} + +export interface ISaveOptions { + + /** + * An indicator how the save operation was triggered. + */ + reason?: SaveReason; + + /** + * Forces to load the contents of the working copy + * again even if the working copy is not dirty. + */ + force?: boolean; + + /** + * Instructs the save operation to skip any save participants. + */ + skipSaveParticipants?: boolean; + + /** + * A hint as to which file systems should be available for saving. + */ + availableFileSystems?: string[]; +} + +export interface IRevertOptions { + + /** + * Forces to load the contents of the working copy + * again even if the working copy is not dirty. + */ + force?: boolean; + + /** + * A soft revert will clear dirty state of a working copy + * but will not attempt to load it from its persisted state. + */ + soft?: boolean; +} + export interface IEditorInput extends IDisposable { /** @@ -330,9 +391,11 @@ export interface IEditorInput extends IDisposable { isDirty(): boolean; /** - * Saves the editor. + * Saves the editor. The provided groupId helps + * implementors to e.g. preserve view state of the editor + * and re-open it in the correct group after saving. */ - save(options?: ISaveOptions): Promise; + save(groupId: GroupIdentifier, options?: ISaveOptions): Promise; /** * Saves the editor to a different location. The provided groupId @@ -374,38 +437,20 @@ export abstract class EditorInput extends Disposable implements IEditorInput { private disposed: boolean = false; - /** - * Returns the unique type identifier of this input. - */ abstract getTypeId(): string; - /** - * Returns the associated resource of this input if any. - */ getResource(): URI | undefined { return undefined; } - /** - * Returns the name of this input that can be shown to the user. Examples include showing the name of the input - * above the editor area when the input is shown. - */ getName(): string { return `Editor ${this.getTypeId()}`; } - /** - * Returns the description of this input that can be shown to the user. Examples include showing the description of - * the input above the editor area to the side of the name of the input. - */ getDescription(verbosity?: Verbosity): string | undefined { return undefined; } - /** - * Returns the title of this input that can be shown to the user. Examples include showing the title of - * the input above the editor area as hover over the input label. - */ getTitle(verbosity?: Verbosity): string { return this.getName(); } @@ -419,10 +464,10 @@ export abstract class EditorInput extends Disposable implements IEditorInput { } /** - * Returns a descriptor suitable for telemetry events. - * - * Subclasses should extend if they can contribute. - */ + * Returns a descriptor suitable for telemetry events. + * + * Subclasses should extend if they can contribute. + */ getTelemetryDescriptor(): { [key: string]: unknown } { /* __GDPR__FRAGMENT__ "EditorTelemetryDescriptor" : { @@ -438,46 +483,26 @@ export abstract class EditorInput extends Disposable implements IEditorInput { */ abstract resolve(): Promise; - /** - * Returns if this input is readonly or not. - */ isReadonly(): boolean { - // Subclasses need to explicitly opt-in to being editable. - return !this.isDirty(); + return true; } - /** - * Returns if the input is an untitled editor or not. - */ isUntitled(): boolean { - // Subclasses need to explicitly opt-in to being untitled. return false; } - /** - * An editor that is dirty will be asked to be saved once it closes. - */ isDirty(): boolean { return false; } - /** - * Saves the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation. - */ - save(options?: ISaveOptions): Promise { + save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { return Promise.resolve(true); } - /** - * Saves the editor to a different location. - */ saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { return Promise.resolve(true); } - /** - * Reverts the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation. - */ revert(options?: IRevertOptions): Promise { return Promise.resolve(true); } @@ -489,24 +514,14 @@ export abstract class EditorInput extends Disposable implements IEditorInput { return true; } - /** - * Returns true if this input is identical to the otherInput. - */ matches(otherInput: unknown): boolean { return this === otherInput; } - /** - * Returns whether this input was disposed or not. - */ isDisposed(): boolean { return this.disposed; } - /** - * Called when an editor input is no longer needed. Allows to free up any resources taken by - * resolving the editor input. - */ dispose(): void { this.disposed = true; this._onDispose.fire(); @@ -530,15 +545,15 @@ export abstract class TextEditorInput extends EditorInput { return this.resource; } - save(options?: ITextFileSaveOptions): Promise { + save(groupId: GroupIdentifier, options?: ITextFileSaveOptions): Promise { return this.textFileService.save(this.resource, options); } saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { - return this.doSaveAs(group, options); + return this.doSaveAs(group, () => this.textFileService.saveAs(this.resource, undefined, options)); } - protected async doSaveAs(group: GroupIdentifier, options?: ITextFileSaveOptions, replaceAllEditors?: boolean): Promise { + protected async doSaveAs(group: GroupIdentifier, saveRunnable: () => Promise, replaceAllEditors?: boolean): Promise { // Preserve view state by opening the editor first. In addition // this allows the user to review the contents of the editor. @@ -549,7 +564,7 @@ export abstract class TextEditorInput extends EditorInput { } // Save as - const target = await this.textFileService.saveAs(this.resource, undefined, options); + const target = await saveRunnable(); if (!target) { return false; // save cancelled } @@ -557,10 +572,10 @@ export abstract class TextEditorInput extends EditorInput { // Replace editor preserving viewstate (either across all groups or // only selected group) if the target is different from the current resource if (!isEqual(target, this.resource)) { - const replacement: IResourceInput = { resource: target, options: { pinned: true, viewState } }; + const replacement = this.editorService.createInput({ resource: target }); const targetGroups = replaceAllEditors ? this.editorGroupService.groups.map(group => group.id) : [group]; for (const group of targetGroups) { - await this.editorService.replaceEditors([{ editor: { resource: this.resource }, replacement }], group); + await this.editorService.replaceEditors([{ editor: this, replacement, options: { pinned: true, viewState } }], group); } } @@ -667,8 +682,8 @@ export class SideBySideEditorInput extends EditorInput { return this.master.isDirty(); } - save(options?: ISaveOptions): Promise { - return this.master.save(options); + save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + return this.master.save(groupId, options); } saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { diff --git a/src/vs/workbench/common/editor/untitledTextEditorInput.ts b/src/vs/workbench/common/editor/untitledTextEditorInput.ts index 9a7d62d909c..8be428a4931 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorInput.ts @@ -7,17 +7,17 @@ import { URI } from 'vs/base/common/uri'; import { suggestFilename } from 'vs/base/common/mime'; import { createMemoizer } from 'vs/base/common/decorators'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; -import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; -import { IEncodingSupport, EncodingMode, Verbosity, IModeSupport, TextEditorInput, GroupIdentifier } from 'vs/workbench/common/editor'; +import { basenameOrAuthority, dirname, toLocalResource } from 'vs/base/common/resources'; +import { IEncodingSupport, EncodingMode, Verbosity, IModeSupport, TextEditorInput, GroupIdentifier, IRevertOptions } from 'vs/workbench/common/editor'; import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Emitter } from 'vs/base/common/event'; import { ITextFileService, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { ILabelService } from 'vs/platform/label/common/label'; import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; -import { IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; /** * An editor input to be used for untitled text buffers. @@ -47,7 +47,8 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin @ITextFileService textFileService: ITextFileService, @ILabelService private readonly labelService: ILabelService, @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { super(resource, editorService, editorGroupService, textFileService); @@ -161,8 +162,27 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin return false; } + save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + return this.doSaveAs(group, async () => { + + // With associated file path, save to the path that is + // associated. Make sure to convert the result using + // remote authority properly. + if (this.hasAssociatedFilePath) { + if (await this.textFileService.save(this.resource, options)) { + return toLocalResource(this.resource, this.environmentService.configuration.remoteAuthority); + } + + return; + } + + // Without associated file path, do a normal "Save As" + return this.textFileService.saveAs(this.resource, undefined, options); + }, true /* replace editor across all groups */); + } + saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { - return this.doSaveAs(group, options, true /* replace editor across all groups */); + return this.doSaveAs(group, () => this.textFileService.saveAs(this.resource, undefined, options), true /* replace editor across all groups */); } revert(options?: IRevertOptions): Promise { diff --git a/src/vs/workbench/common/editor/untitledTextEditorModel.ts b/src/vs/workbench/common/editor/untitledTextEditorModel.ts index 280ad2c1068..ccb30868d92 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEncodingSupport } from 'vs/workbench/common/editor'; +import { IEncodingSupport, ISaveOptions } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { URI } from 'vs/base/common/uri'; import { CONTENT_CHANGE_EVENT_BUFFER_DELAY } from 'vs/platform/files/common/files'; @@ -16,7 +16,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/res import { ITextBufferFactory } from 'vs/editor/common/model'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; -import { IWorkingCopyService, IWorkingCopy, ISaveOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; export class UntitledTextEditorModel extends BaseTextEditorModel implements IEncodingSupport, IWorkingCopy { diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index e03100082b0..df5446fd52b 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -8,10 +8,10 @@ import * as peekView from 'vs/editor/contrib/peekView/peekView'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { CallHierarchyDirection, CallHierarchyModel } from 'vs/workbench/contrib/callHierarchy/browser/callHierarchy'; -import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { WorkbenchAsyncDataTree, IWorkbenchAsyncDataTreeOptions } from 'vs/platform/list/browser/listService'; import { FuzzyScore } from 'vs/base/common/filters'; import * as callHTree from 'vs/workbench/contrib/callHierarchy/browser/callHierarchyTree'; -import { IAsyncDataTreeOptions, IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; import { localize } from 'vs/nls'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -194,11 +194,14 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { const treeContainer = document.createElement('div'); addClass(treeContainer, 'tree'); container.appendChild(treeContainer); - const options: IAsyncDataTreeOptions = { + const options: IWorkbenchAsyncDataTreeOptions = { sorter: new callHTree.Sorter(), identityProvider: new callHTree.IdentityProvider(() => this._direction), ariaLabel: localize('tree.aria', "Call Hierarchy"), expandOnlyOnTwistieClick: true, + overrideStyles: { + listBackground: peekView.peekViewResultsBackground + } }; this._tree = this._instantiationService.createInstance>( WorkbenchAsyncDataTree, diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 747f33c052c..9a2e4342f70 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -12,11 +12,10 @@ import { DataUri, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IEditorInput, Verbosity, GroupIdentifier } from 'vs/workbench/common/editor'; +import { IEditorInput, Verbosity, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { CustomEditorModel } from '../common/customEditorModel'; -import { ISaveOptions, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { @@ -125,7 +124,7 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return this._model ? this._model.isDirty() : false; } - public save(options?: ISaveOptions): Promise { + public save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { return this._model ? this._model.save(options) : Promise.resolve(false); } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts index 9a34c46ead5..2d1988748da 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts @@ -7,8 +7,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICustomEditorModel, CustomEditorEdit } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { IRevertOptions, ISaveOptions, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; - +import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; export class CustomEditorModel extends Disposable implements ICustomEditorModel { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index a23fddadbb6..789df7edfd6 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -31,6 +31,7 @@ import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/ import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; const $ = dom.$; @@ -85,6 +86,9 @@ export class BreakpointsView extends ViewletPanel { getPosInSet: (_: IEnablement, index: number) => index, getRole: (breakpoint: IEnablement) => 'checkbox', isChecked: (breakpoint: IEnablement) => breakpoint.enabled + }, + overrideStyles: { + listBackground: SIDE_BAR_BACKGROUND } }); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 528bffc3b13..667f578a5f1 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -34,6 +34,7 @@ import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; import { STOP_ID, STOP_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, RESTART_SESSION_ID, RESTART_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STEP_INTO_LABEL, STEP_INTO_ID, STEP_OUT_LABEL, STEP_OUT_ID, PAUSE_ID, PAUSE_LABEL, CONTINUE_ID, CONTINUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { CollapseAction } from 'vs/workbench/browser/viewlet'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; const $ = dom.$; @@ -161,7 +162,10 @@ export class CallStackView extends ViewletPanel { return nls.localize('showMoreStackFrames2', "Show More Stack Frames"); } }, - expandOnlyOnTwistieClick: true + expandOnlyOnTwistieClick: true, + overrideStyles: { + listBackground: SIDE_BAR_BACKGROUND + } }); this.tree.setInput(this.debugService.getModel()); diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index 99dcef5b9cd..9efb9d69461 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -78,7 +78,10 @@ export class DebugHoverWidget implements IContentWidget { ariaLabel: nls.localize('treeAriaLabel', "Debug Hover"), accessibilityProvider: new DebugHoverAccessibilityProvider(), mouseSupport: false, - horizontalScrolling: true + horizontalScrolling: true, + overrideStyles: { + listBackground: editorHoverBackground + } }); this.valueContainer = $('.value'); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 94c07e70c9c..24a3b0902e9 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -581,19 +581,19 @@ export class DebugService implements IDebugService { return this.runTaskAndCheckErrors(session.root, session.configuration.preLaunchTask); }; - if (session.capabilities.supportsRestartRequest) { + if (isExtensionHostDebugging(session.configuration)) { const taskResult = await runTasks(); if (taskResult === TaskRunResult.Success) { - await session.restart(); + this.extensionHostDebugService.reload(session.getId()); } return; } - if (isExtensionHostDebugging(session.configuration)) { + if (session.capabilities.supportsRestartRequest) { const taskResult = await runTasks(); if (taskResult === TaskRunResult.Success) { - this.extensionHostDebugService.reload(session.getId()); + await session.restart(); } return; diff --git a/src/vs/workbench/contrib/debug/browser/linkDetector.ts b/src/vs/workbench/contrib/debug/browser/linkDetector.ts index 0d115223420..030231a132f 100644 --- a/src/vs/workbench/contrib/debug/browser/linkDetector.ts +++ b/src/vs/workbench/contrib/debug/browser/linkDetector.ts @@ -12,6 +12,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f'; const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|data:|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug'); @@ -37,7 +38,8 @@ export class LinkDetector { @IEditorService private readonly editorService: IEditorService, @IFileService private readonly fileService: IFileService, @IOpenerService private readonly openerService: IOpenerService, - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService ) { // noop } @@ -96,7 +98,7 @@ export class LinkDetector { private createWebLink(url: string): Node { const link = this.createLink(url); const uri = URI.parse(url); - this.decorateLink(link, () => this.openerService.open(uri)); + this.decorateLink(link, () => this.openerService.open(uri, { allowTunneling: !!this.workbenchEnvironmentService.configuration.remoteAuthority })); return link; } diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index 5cc13614bb1..28c7604d2a0 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -34,6 +34,7 @@ import { dispose } from 'vs/base/common/lifecycle'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; import { ILabelService } from 'vs/platform/label/common/label'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; const SMART = true; @@ -440,6 +441,9 @@ export class LoadedScriptsView extends ViewletPanel { filter: this.filter, accessibilityProvider: new LoadedSciptsAccessibilityProvider(), ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'loadedScriptsAriaLabel' }, "Debug Loaded Scripts"), + overrideStyles: { + listBackground: SIDE_BAR_BACKGROUND + } } ); diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 88b1a920272..b0394820606 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -63,6 +63,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; const $ = dom.$; @@ -428,7 +429,10 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IReplElement) => e }, horizontalScrolling: !wordWrap, setRowLineHeight: false, - supportDynamicHeights: wordWrap + supportDynamicHeights: wordWrap, + overrideStyles: { + listBackground: PANEL_BACKGROUND + } }); this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); let lastSelectedString: string; diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index e84983e6c7b..a6104ac4e9b 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -30,6 +30,7 @@ import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabe import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; const $ = dom.$; let forgetScopes = true; @@ -91,7 +92,10 @@ export class VariablesView extends ViewletPanel { ariaLabel: nls.localize('variablesAriaTreeLabel', "Debug Variables"), accessibilityProvider: new VariablesAccessibilityProvider(), identityProvider: { getId: (element: IExpression | IScope) => element.getId() }, - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression | IScope) => e } + keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression | IScope) => e }, + overrideStyles: { + listBackground: SIDE_BAR_BACKGROUND + } }); this.tree.setInput(this.debugService.getViewModel()); diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index e416cc5565a..2a601f78e9d 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -30,6 +30,7 @@ import { IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel import { variableSetEmitter, VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; @@ -68,6 +69,9 @@ export class WatchExpressionsView extends ViewletPanel { identityProvider: { getId: (element: IExpression) => element.getId() }, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression) => e }, dnd: new WatchExpressionsDragAndDrop(this.debugService), + overrideStyles: { + listBackground: SIDE_BAR_BACKGROUND + } }); this.tree.setInput(this.debugService); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 032d8c5ddcd..32abad22c0f 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -26,7 +26,7 @@ import { Schemas } from 'vs/base/common/network'; import { WorkspaceFolderCountContext, IsWebContext } from 'vs/workbench/browser/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { OpenFileFolderAction, OpenFileAction, OpenFolderAction, OpenWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; -import { ActiveEditorIsSaveableContext, DirtyWorkingCopiesContext } from 'vs/workbench/common/editor'; +import { ActiveEditorIsSaveableContext, DirtyWorkingCopiesContext, ActiveEditorContext } from 'vs/workbench/common/editor'; import { SidebarFocusContext } from 'vs/workbench/common/viewlet'; import { registerAndGetAmdImageURL } from 'vs/base/common/amd'; @@ -590,7 +590,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { command: { id: SAVE_FILE_AS_COMMAND_ID, title: nls.localize({ key: 'miSaveAs', comment: ['&& denotes a mnemonic'] }, "Save &&As..."), - precondition: ContextKeyExpr.or(ActiveEditorIsSaveableContext, ContextKeyExpr.and(ExplorerViewletVisibleContext, SidebarFocusContext)) + // ActiveEditorContext is not 100% correct, but we lack a context for indicating "Save As..." support + precondition: ContextKeyExpr.or(ActiveEditorContext, ContextKeyExpr.and(ExplorerViewletVisibleContext, SidebarFocusContext)) }, order: 2 }); diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index a3c9bae7a01..b638a347cba 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { toResource, IEditorCommandsContext, SideBySideEditor, IEditorIdentifier } from 'vs/workbench/common/editor'; +import { toResource, IEditorCommandsContext, SideBySideEditor, IEditorIdentifier, SaveReason } from 'vs/workbench/common/editor'; import { IWindowOpenable, IOpenWindowOptions, isWorkspaceToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -36,6 +36,7 @@ import { basename, joinPath, isEqual } from 'vs/base/common/resources'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { UNTITLED_WORKSPACE_NAME } from 'vs/platform/workspaces/common/workspaces'; +import { coalesce } from 'vs/base/common/arrays'; // Commands @@ -313,16 +314,19 @@ function saveSelectedEditors(accessor: ServicesAccessor, options?: ISaveEditorsO const listService = accessor.get(IListService); const editorGroupsService = accessor.get(IEditorGroupsService); - const saveableEditors = getMultiSelectedEditors(listService, editorGroupsService).filter(({ editor }) => !editor.isReadonly()); + let saveableEditors = getMultiSelectedEditors(listService, editorGroupsService); + if (!options?.saveAs) { + saveableEditors = saveableEditors.filter(({ editor }) => !editor.isReadonly()); // Save: only allow non-readonly editors + } return doSaveEditors(accessor, saveableEditors, options); } -function saveEditorsOfGroups(accessor: ServicesAccessor, groups = accessor.get(IEditorGroupsService).getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE), options?: ISaveEditorsOptions): Promise { +function saveEditorsOfGroups(accessor: ServicesAccessor, groups: ReadonlyArray, options?: ISaveEditorsOptions): Promise { const saveableEditors: IEditorIdentifier[] = []; for (const group of groups) { for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { - if (editor.isDirty()) { + if (editor.isDirty() && !editor.isReadonly()) { saveableEditors.push({ groupId: group.id, editor }); } } @@ -348,7 +352,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ primary: KeyMod.CtrlCmd | KeyCode.KEY_S, id: SAVE_FILE_COMMAND_ID, handler: accessor => { - return saveSelectedEditors(accessor, { force: true }); + return saveSelectedEditors(accessor, { reason: SaveReason.EXPLICIT, force: true }); } }); @@ -359,7 +363,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S) }, id: SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, handler: accessor => { - return saveSelectedEditors(accessor, { force: true, skipSaveParticipants: true }); + return saveSelectedEditors(accessor, { reason: SaveReason.EXPLICIT, force: true, skipSaveParticipants: true }); } }); @@ -369,14 +373,14 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: undefined, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S, handler: accessor => { - return saveSelectedEditors(accessor, { saveAs: true }); + return saveSelectedEditors(accessor, { reason: SaveReason.EXPLICIT, saveAs: true }); } }); CommandsRegistry.registerCommand({ id: SAVE_ALL_COMMAND_ID, handler: (accessor) => { - return saveEditorsOfGroups(accessor); + return saveEditorsOfGroups(accessor, accessor.get(IEditorGroupsService).getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE), { reason: SaveReason.EXPLICIT }); } }); @@ -387,23 +391,14 @@ CommandsRegistry.registerCommand({ const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService), accessor.get(IEditorGroupsService)); - let groups: IEditorGroup[] | undefined = undefined; + let groups: ReadonlyArray | undefined = undefined; if (!contexts.length) { - groups = [...editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)]; + groups = editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); } else { - contexts.forEach(context => { - const editorGroup = editorGroupService.getGroup(context.groupId); - if (editorGroup) { - if (!groups) { - groups = []; - } - - groups.push(editorGroup); - } - }); + groups = coalesce(contexts.map(context => editorGroupService.getGroup(context.groupId))); } - return saveEditorsOfGroups(accessor, groups); + return saveEditorsOfGroups(accessor, groups, { reason: SaveReason.EXPLICIT }); } }); @@ -412,7 +407,7 @@ CommandsRegistry.registerCommand({ handler: accessor => { const editorService = accessor.get(IEditorService); - return editorService.saveAll({ includeUntitled: false }); + return editorService.saveAll({ includeUntitled: false, reason: SaveReason.EXPLICIT }); } }); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index ee0dfa24cd3..e8727ebc798 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -426,9 +426,9 @@ configurationRegistry.registerConfiguration({ description: nls.localize('explorer.incrementalNaming', "Controls what naming strategy to use when a giving a new name to a duplicated explorer item on paste."), default: 'simple' }, - 'explorer.compressSingleChildFolders': { + 'explorer.compactFolders': { 'type': 'boolean', - 'description': nls.localize('compressSingleChildFolders', "Controls whether the explorer should compress single child folders in a combined tree element. Useful for Java project folder structures, for example."), + 'description': nls.localize('compressSingleChildFolders', "Controls whether the explorer should render folders in a compact form. In such a form, single child folders will be compressed in a combined tree element. Useful for Java package structures, for example."), 'default': true }, } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 188f4eb91b4..010f7e545d3 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -54,6 +54,7 @@ import { Event } from 'vs/base/common/event'; import { attachStyler, IColorMapping } from 'vs/platform/theme/common/styler'; import { ColorValue, listDropBackground } from 'vs/platform/theme/common/colorRegistry'; import { Color } from 'vs/base/common/color'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; interface IExplorerViewColors extends IColorMapping { listDropBackground?: ColorValue | undefined; @@ -344,7 +345,7 @@ export class ExplorerView extends ViewletPanel { this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - const isCompressionEnabled = () => this.configurationService.getValue('explorer.compressSingleChildFolders'); + const isCompressionEnabled = () => this.configurationService.getValue('explorer.compactFolders'); this.tree = this.instantiationService.createInstance>(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), new ExplorerCompressionDelegate(), [this.renderer], this.instantiationService.createInstance(ExplorerDataSource), { @@ -381,12 +382,15 @@ export class ExplorerView extends ViewletPanel { sorter: this.instantiationService.createInstance(FileSorter), dnd: this.instantiationService.createInstance(FileDragAndDrop), autoExpandSingleChildren: true, - additionalScrollHeight: ExplorerDelegate.ITEM_HEIGHT + additionalScrollHeight: ExplorerDelegate.ITEM_HEIGHT, + overrideStyles: { + listBackground: SIDE_BAR_BACKGROUND + } }); this._register(this.tree); // Bind configuration - const onDidChangeCompressionConfiguration = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('explorer.compressSingleChildFolders')); + const onDidChangeCompressionConfiguration = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('explorer.compactFolders')); this._register(onDidChangeCompressionConfiguration(_ => this.tree.updateOptions({ compressionEnabled: isCompressionEnabled() }))); // Bind context keys diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 733c710903f..8bb84ec2795 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -253,8 +253,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer e.name); - const editableData = this.explorerService.getEditableData(stat); + const editable = node.element.elements.filter(e => this.explorerService.isEditable(e)); + const editableData = editable.length === 0 ? undefined : this.explorerService.getEditableData(editable[0]); // File Label if (!editableData) { @@ -262,6 +262,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer e.name); disposables.add(this.renderStat(stat, label, node.filterData, templateData)); const compressedNavigationController = new CompressedNavigationController(node.element.elements, templateData); @@ -286,7 +287,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer element instanceof OpenEditor ? element.getId() : element.id.toString() }, - dnd: new OpenEditorsDragAndDrop(this.instantiationService, this.editorGroupService) + dnd: new OpenEditorsDragAndDrop(this.instantiationService, this.editorGroupService), + overrideStyles: { + listBackground: SIDE_BAR_BACKGROUND + } }); this._register(this.list); this._register(this.listLabels); diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index 83ba201f267..387acd584b5 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -7,8 +7,7 @@ import { localize } from 'vs/nls'; import { createMemoizer } from 'vs/base/common/decorators'; import { dirname } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { EncodingMode, IFileEditorInput, ITextEditorModel, Verbosity, TextEditorInput } from 'vs/workbench/common/editor'; -import { IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { EncodingMode, IFileEditorInput, ITextEditorModel, Verbosity, TextEditorInput, IRevertOptions } from 'vs/workbench/common/editor'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { FileOperationError, FileOperationResult, IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; @@ -225,7 +224,9 @@ export class FileEditorInput extends TextEditorInput implements IFileEditorInput } isReadonly(): boolean { - return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + const model = this.textFileService.models.get(this.resource); + + return model?.isReadonly() || this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } isDirty(): boolean { diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 674d1f2a1e4..35e56e70a5f 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -141,7 +141,7 @@ suite('Files - FileEditorInput', () => { resolved.textEditorModel!.setValue('changed'); assert.ok(input.isDirty()); - await input.save(); + await input.save(0); assert.ok(!input.isDirty()); resolved.dispose(); }); diff --git a/src/vs/workbench/contrib/outline/browser/outlinePanel.ts b/src/vs/workbench/contrib/outline/browser/outlinePanel.ts index 75777a0e8e7..9d563e905c0 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePanel.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePanel.ts @@ -47,6 +47,7 @@ import { basename } from 'vs/base/common/resources'; import { IDataSource } from 'vs/base/browser/ui/tree/tree'; import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; class RequestState { @@ -331,7 +332,10 @@ export class OutlinePanel extends ViewletPanel { filter: this._treeFilter, identityProvider: new OutlineIdentityProvider(), keyboardNavigationLabelProvider: new OutlineNavigationLabelProvider(), - hideTwistiesOfChildlessElements: true + hideTwistiesOfChildlessElements: true, + overrideStyles: { + listBackground: SIDE_BAR_BACKGROUND + } } ); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts index 144a775d045..77284a59676 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -255,7 +255,7 @@ export class PreferencesEditor extends BaseEditor { if (this.editorService.activeControl !== this) { this.focus(); } - const promise: Promise = this.input && this.input.isDirty() ? this.input.save() : Promise.resolve(true); + const promise: Promise = this.input && this.input.isDirty() ? this.input.save(this.group!.id) : Promise.resolve(true); promise.then(() => { if (target === ConfigurationTarget.USER_LOCAL) { this.preferencesService.switchSettings(ConfigurationTarget.USER_LOCAL, this.preferencesService.userSettingsResource, true); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 1d61e9073c2..103536ddc96 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -1019,7 +1019,7 @@ class UnsupportedSettingsRenderer extends Disposable { severity: MarkerSeverity.Hint, tags: [MarkerTag.Unnecessary], ...setting.range, - message: nls.localize('unsupportedRemoteMachineSetting', "This setting cannot be applied now. It will be applied when you open local window.") + message: nls.localize('unsupportedRemoteMachineSetting', "This setting cannot be applied in this window. It will be applied when you open local window.") }); } } @@ -1054,7 +1054,7 @@ class UnsupportedSettingsRenderer extends Disposable { severity: MarkerSeverity.Hint, tags: [MarkerTag.Unnecessary], ...setting.range, - message: nls.localize('unsupportedWindowSetting', "This setting cannot be applied now. It will be applied when you open this folder directly.") + message: nls.localize('unsupportedWindowSetting', "This setting cannot be applied in this workspace. It will be applied when you open the containing workspace folder directly.") }); } } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index b05433b5f86..65b5fb097c1 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -555,7 +555,7 @@ export class SettingsEditor2 extends BaseEditor { this.createFocusSink( bodyContainer, e => { - if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { + if (DOM.findParentWithClass(e.relatedTarget, 'monaco-list')) { if (this.settingsTree.scrollTop > 0) { const firstElement = this.settingsTree.firstVisibleElement; this.settingsTree.reveal(firstElement, 0.1); @@ -577,7 +577,7 @@ export class SettingsEditor2 extends BaseEditor { this.createFocusSink( bodyContainer, e => { - if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { + if (DOM.findParentWithClass(e.relatedTarget, 'monaco-list')) { if (this.settingsTree.scrollTop < this.settingsTree.scrollHeight) { const lastElement = this.settingsTree.lastVisibleElement; this.settingsTree.reveal(lastElement, 0.9); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 10c4a7e5af1..fff4ef6068b 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -147,7 +147,7 @@ export const tocData: ITOCEntry = { }, { id: 'features/extensions', - label: localize('extensionViewlet', "Extension Viewlet"), + label: localize('extensions', "Extensions"), settings: ['extensions.*'] }, { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index a722df7c506..f732f7672dd 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -1451,8 +1451,6 @@ export class SettingsTree extends ObjectTree { @IThemeService themeService: IThemeService, @IInstantiationService instantiationService: IInstantiationService, ) { - const treeClass = 'settings-editor-tree'; - super('SettingsTree', container, new SettingsTreeDelegate(), renderers, @@ -1465,7 +1463,7 @@ export class SettingsTree extends ObjectTree { return e.id; } }, - styleController: new DefaultStyleController(DOM.createStyleSheet(container), treeClass), + styleController: id => new DefaultStyleController(DOM.createStyleSheet(container), id), filter: instantiationService.createInstance(SettingsTreeFilter, viewState) }); @@ -1518,8 +1516,6 @@ export class SettingsTree extends ObjectTree { } })); - this.getHTMLElement().classList.add(treeClass); - this.disposables.add(attachStyler(themeService, { listActiveSelectionBackground: transparent(Color.white, 0), listActiveSelectionForeground: foreground, diff --git a/src/vs/workbench/contrib/preferences/browser/tocTree.ts b/src/vs/workbench/contrib/preferences/browser/tocTree.ts index 845274f20ec..301eaf966c5 100644 --- a/src/vs/workbench/contrib/preferences/browser/tocTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/tocTree.ts @@ -189,7 +189,6 @@ export class TOCTree extends ObjectTree { ) { // test open mode - const treeClass = 'settings-toc-tree'; const filter = instantiationService.createInstance(SettingsTreeFilter, viewState); const options: IObjectTreeOptions = { filter, @@ -199,7 +198,7 @@ export class TOCTree extends ObjectTree { return e.id; } }, - styleController: new DefaultStyleController(DOM.createStyleSheet(container), treeClass), + styleController: id => new DefaultStyleController(DOM.createStyleSheet(container), id), accessibilityProvider: instantiationService.createInstance(SettingsAccessibilityProvider), collapseByDefault: true }; @@ -209,8 +208,6 @@ export class TOCTree extends ObjectTree { [new TOCRenderer()], options); - this.getHTMLElement().classList.add(treeClass); - this.disposables.add(attachStyler(themeService, { listActiveSelectionBackground: editorBackground, listActiveSelectionForeground: settingsHeaderForeground, diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index 54888ea75b7..d2d8f54ccb5 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { MarkdownString } from 'vs/base/common/htmlContent'; -import { compare } from 'vs/base/common/strings'; +import { compare, startsWith } from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; @@ -22,14 +22,14 @@ export class SnippetCompletion implements CompletionItem { detail: string; insertText: string; documentation?: MarkdownString; - range: IRange; + range: IRange | { insert: IRange, replace: IRange }; sortText: string; kind: CompletionItemKind; insertTextRules: CompletionItemInsertTextRule; constructor( readonly snippet: Snippet, - range: IRange + range: IRange | { insert: IRange, replace: IRange } ) { this.label = snippet.prefix; this.detail = localize('detail.snippet', "{0} ({1})", snippet.description || snippet.name, snippet.source); @@ -80,7 +80,8 @@ export class SnippetCompletionProvider implements CompletionItemProvider { let suggestions: SnippetCompletion[]; let pos = { lineNumber: position.lineNumber, column: 1 }; let lineOffsets: number[] = []; - let linePrefixLow = model.getLineContent(position.lineNumber).substr(0, position.column - 1).toLowerCase(); + const lineContent = model.getLineContent(position.lineNumber); + const linePrefixLow = lineContent.substr(0, position.column - 1).toLowerCase(); let endsInWhitespace = linePrefixLow.match(/\s$/); while (pos.column < position.column) { @@ -104,13 +105,19 @@ export class SnippetCompletionProvider implements CompletionItemProvider { } } + const lineSuffixLow = lineContent.substr(position.column - 1).toLowerCase(); let availableSnippets = new Set(); snippets.forEach(availableSnippets.add, availableSnippets); suggestions = []; for (let start of lineOffsets) { availableSnippets.forEach(snippet => { if (isPatternInWord(linePrefixLow, start, linePrefixLow.length, snippet.prefixLow, 0, snippet.prefixLow.length)) { - suggestions.push(new SnippetCompletion(snippet, Range.fromPositions(position.delta(0, -(linePrefixLow.length - start)), position))); + const snippetPrefixSubstr = snippet.prefixLow.substr(linePrefixLow.length - start); + const endColumn = startsWith(lineSuffixLow, snippetPrefixSubstr) ? position.column + snippetPrefixSubstr.length : position.column; + const replace = Range.fromPositions(position.delta(0, -(linePrefixLow.length - start)), { lineNumber: position.lineNumber, column: endColumn }); + const insert = replace.setEndPosition(position.lineNumber, position.column); + + suggestions.push(new SnippetCompletion(snippet, { replace, insert })); availableSnippets.delete(snippet); } }); @@ -119,7 +126,8 @@ export class SnippetCompletionProvider implements CompletionItemProvider { // add remaing snippets when the current prefix ends in whitespace or when no // interesting positions have been found availableSnippets.forEach(snippet => { - suggestions.push(new SnippetCompletion(snippet, Range.fromPositions(position))); + const range = Range.fromPositions(position); + suggestions.push(new SnippetCompletion(snippet, { replace: range, insert: range })); }); } diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index f120a12e2d9..fd90cd10897 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -84,7 +84,7 @@ suite('SnippetsService', function () { assert.equal(result.incomplete, undefined); assert.equal(result.suggestions.length, 1); assert.equal(result.suggestions[0].label, 'bar'); - assert.equal((result.suggestions[0].range as any).startColumn, 1); + assert.equal((result.suggestions[0].range as any).insert.startColumn, 1); assert.equal(result.suggestions[0].insertText, 'barCodeSnippet'); }); }); @@ -117,10 +117,10 @@ suite('SnippetsService', function () { assert.equal(result.suggestions.length, 2); assert.equal(result.suggestions[0].label, 'bar'); assert.equal(result.suggestions[0].insertText, 's1'); - assert.equal((result.suggestions[0].range as any).startColumn, 1); + assert.equal((result.suggestions[0].range as any).insert.startColumn, 1); assert.equal(result.suggestions[1].label, 'bar-bar'); assert.equal(result.suggestions[1].insertText, 's2'); - assert.equal((result.suggestions[1].range as any).startColumn, 1); + assert.equal((result.suggestions[1].range as any).insert.startColumn, 1); }); await provider.provideCompletionItems(model, new Position(1, 5), context)!.then(result => { @@ -128,7 +128,7 @@ suite('SnippetsService', function () { assert.equal(result.suggestions.length, 1); assert.equal(result.suggestions[0].label, 'bar-bar'); assert.equal(result.suggestions[0].insertText, 's2'); - assert.equal((result.suggestions[0].range as any).startColumn, 1); + assert.equal((result.suggestions[0].range as any).insert.startColumn, 1); }); await provider.provideCompletionItems(model, new Position(1, 6), context)!.then(result => { @@ -136,10 +136,10 @@ suite('SnippetsService', function () { assert.equal(result.suggestions.length, 2); assert.equal(result.suggestions[0].label, 'bar'); assert.equal(result.suggestions[0].insertText, 's1'); - assert.equal((result.suggestions[0].range as any).startColumn, 5); + assert.equal((result.suggestions[0].range as any).insert.startColumn, 5); assert.equal(result.suggestions[1].label, 'bar-bar'); assert.equal(result.suggestions[1].insertText, 's2'); - assert.equal((result.suggestions[1].range as any).startColumn, 1); + assert.equal((result.suggestions[1].range as any).insert.startColumn, 1); }); }); @@ -165,14 +165,14 @@ suite('SnippetsService', function () { return provider.provideCompletionItems(model, new Position(1, 4), context)!; }).then(result => { assert.equal(result.suggestions.length, 1); - assert.equal((result.suggestions[0].range as any).startColumn, 2); + assert.equal((result.suggestions[0].range as any).insert.startColumn, 2); model.dispose(); model = TextModel.createFromString('a { assert.equal(result.suggestions.length, 1); - assert.equal((result.suggestions[0].range as any).startColumn, 2); + assert.equal((result.suggestions[0].range as any).insert.startColumn, 2); model.dispose(); }); }); @@ -400,13 +400,43 @@ suite('SnippetsService', function () { assert.equal(result.suggestions.length, 1); let [first] = result.suggestions; - assert.equal((first.range as any).startColumn, 2); + assert.equal((first.range as any).insert.startColumn, 2); model = TextModel.createFromString('1', undefined, modeService.getLanguageIdentifier('fooLang')); result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.equal(result.suggestions.length, 1); [first] = result.suggestions; - assert.equal((first.range as any).startColumn, 1); + assert.equal((first.range as any).insert.startColumn, 1); + }); + + test('Snippet replace range', async function () { + snippetService = new SimpleSnippetService([new Snippet( + ['fooLang'], + 'notWordTest', + 'not word', + '', + 'not word snippet', + '', + SnippetSource.User + )]); + + const provider = new SnippetCompletionProvider(modeService, snippetService); + + let model = TextModel.createFromString('not wordFoo bar', undefined, modeService.getLanguageIdentifier('fooLang')); + let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; + + assert.equal(result.suggestions.length, 1); + let [first] = result.suggestions; + assert.equal((first.range as any).insert.endColumn, 3); + assert.equal((first.range as any).replace.endColumn, 9); + + model = TextModel.createFromString('not woFoo bar', undefined, modeService.getLanguageIdentifier('fooLang')); + result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; + + assert.equal(result.suggestions.length, 1); + [first] = result.suggestions; + assert.equal((first.range as any).insert.endColumn, 3); + assert.equal((first.range as any).replace.endColumn, 3); }); }); diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index a01d38e521f..12092eb95e7 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -313,7 +313,7 @@ configurationRegistry.registerConfiguration({ type: 'object', properties: { 'task.problemMatchers.neverPrompt': { - markdownDescription: nls.localize('task.problemMatchers.neverPrompt', "Configures whether to show the problem matcher prompt when running a task. Set to `true` to never promp, or use a dictionary of task types to turn off prompting only for specific task types."), + markdownDescription: nls.localize('task.problemMatchers.neverPrompt', "Configures whether to show the problem matcher prompt when running a task. Set to `true` to never prompt, or use a dictionary of task types to turn off prompting only for specific task types."), 'oneOf': [ { type: 'boolean', diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 2971096e09d..45973bb6d7b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -305,17 +305,7 @@ export class CreateNewWithCwdTerminalCommand extends Command { public runCommand(accessor: ServicesAccessor, args: { cwd: string } | undefined): Promise { const terminalService = accessor.get(ITerminalService); - const configurationResolverService = accessor.get(IConfigurationResolverService); - const workspaceContextService = accessor.get(IWorkspaceContextService); - const historyService = accessor.get(IHistoryService); - const activeWorkspaceRootUri = historyService.getLastActiveWorkspaceRoot(Schemas.file); - const lastActiveWorkspaceRoot = activeWorkspaceRootUri ? withNullAsUndefined(workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; - - let cwd: string | undefined; - if (args && args.cwd) { - cwd = configurationResolverService.resolve(lastActiveWorkspaceRoot, args.cwd); - } - const instance = terminalService.createTerminal({ cwd }); + const instance = terminalService.createTerminal({ cwd: args?.cwd }); if (!instance) { return Promise.resolve(undefined); } diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index 31b85a9ccdd..cb2aebdc148 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -184,23 +184,16 @@ export function getCwd( logService?: ILogService ): string { if (shell.cwd) { - return (typeof shell.cwd === 'object') ? shell.cwd.fsPath : shell.cwd; + const unresolved = (typeof shell.cwd === 'object') ? shell.cwd.fsPath : shell.cwd; + const resolved = _resolveCwd(unresolved, lastActiveWorkspace, configurationResolverService); + return resolved || unresolved; } let cwd: string | undefined; if (!shell.ignoreConfigurationCwd && customCwd) { if (configurationResolverService) { - try { - customCwd = configurationResolverService.resolve(lastActiveWorkspace, customCwd); - } catch (e) { - // There was an issue resolving a variable, log the error in the console and - // fallback to the default. - if (logService) { - logService.error('Could not resolve terminal.integrated.cwd', e); - } - customCwd = undefined; - } + customCwd = _resolveCwd(customCwd, lastActiveWorkspace, configurationResolverService, logService); } if (customCwd) { if (path.isAbsolute(customCwd)) { @@ -219,6 +212,18 @@ export function getCwd( return _sanitizeCwd(cwd); } +function _resolveCwd(cwd: string, lastActiveWorkspace: IWorkspaceFolder | undefined, configurationResolverService: IConfigurationResolverService | undefined, logService?: ILogService): string | undefined { + if (configurationResolverService) { + try { + return configurationResolverService.resolve(lastActiveWorkspace, cwd); + } catch (e) { + logService?.error('Could not resolve terminal cwd', e); + return undefined; + } + } + return cwd; +} + function _sanitizeCwd(cwd: string): string { // Make the drive letter uppercase on Windows (see #9448) if (platform.platform === platform.Platform.Windows && cwd && cwd[1] === ':') { diff --git a/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts b/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts index b3602d62760..661b8412cad 100644 --- a/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts +++ b/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { IEditorInputFactory, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, EditorModel, EditorOptions, GroupIdentifier } from 'vs/workbench/common/editor'; +import { IEditorInputFactory, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, EditorModel, EditorOptions, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { Dimension, addDisposableListener, EventType } from 'vs/base/browser/dom'; @@ -24,7 +24,7 @@ import { isEqual } from 'vs/base/common/resources'; import { generateUuid } from 'vs/base/common/uuid'; import { CancellationToken } from 'vs/base/common/cancellation'; import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IWorkingCopy, IWorkingCopyService, IRevertOptions, ISaveOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { env } from 'vs/base/common/process'; const CUSTOM_SCHEME = 'testCustomEditor'; @@ -168,7 +168,7 @@ class TestCustomEditorInput extends EditorInput implements IWorkingCopy { return this.dirty; } - async save(options?: ISaveOptions): Promise { + async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { this.setDirty(false); return true; diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 5e123f78eaa..24482931109 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -34,6 +34,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { ICommandHandler } from 'vs/platform/commands/common/commands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { normalizeDriveLetter } from 'vs/base/common/labels'; +import { SaveReason } from 'vs/workbench/common/editor'; export namespace OpenLocalFileCommand { export const ID = 'workbench.action.files.openLocalFile'; @@ -54,7 +55,7 @@ export namespace SaveLocalFileCommand { const editorService = accessor.get(IEditorService); const activeControl = editorService.activeControl; if (activeControl) { - return editorService.save({ groupId: activeControl.group.id, editor: activeControl.input }, { saveAs: true, availableFileSystems: [Schemas.file] }); + return editorService.save({ groupId: activeControl.group.id, editor: activeControl.input }, { saveAs: true, availableFileSystems: [Schemas.file], reason: SaveReason.EXPLICIT }); } return Promise.resolve(undefined); diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 3140d640aba..6eec1f2f31c 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -5,7 +5,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IResourceInput, ITextEditorOptions, IEditorOptions, EditorActivation } from 'vs/platform/editor/common/editor'; -import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, toResource, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, toResource, SideBySideEditor, IRevertOptions } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInput'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -28,7 +28,6 @@ import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/wor import { ILabelService } from 'vs/platform/label/common/label'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; type CachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput; type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE; @@ -363,7 +362,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { return neighbourGroup; } - private toOptions(options?: IEditorOptions | EditorOptions): EditorOptions { + private toOptions(options?: IEditorOptions | ITextEditorOptions | EditorOptions): EditorOptions { if (!options || options instanceof EditorOptions) { return options as EditorOptions; } @@ -485,17 +484,20 @@ export class EditorService extends Disposable implements EditorServiceImpl { editors.forEach(replaceEditorArg => { if (replaceEditorArg.editor instanceof EditorInput) { - typedEditors.push(replaceEditorArg as IEditorReplacement); - } else { - const editor = replaceEditorArg.editor as IResourceEditor; - const replacement = replaceEditorArg.replacement as IResourceEditor; - const typedEditor = this.createInput(editor); - const typedReplacement = this.createInput(replacement); + const replacementArg = replaceEditorArg as IEditorReplacement; typedEditors.push({ - editor: typedEditor, - replacement: typedReplacement, - options: this.toOptions(replacement.options) + editor: replacementArg.editor, + replacement: replacementArg.replacement, + options: this.toOptions(replacementArg.options) + }); + } else { + const replacementArg = replaceEditorArg as IResourceEditorReplacement; + + typedEditors.push({ + editor: this.createInput(replacementArg.editor), + replacement: this.createInput(replacementArg.replacement), + options: this.toOptions(replacementArg.replacement.options) }); } }); @@ -691,7 +693,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { this.editorGroupService.getGroup(groupId)?.pinEditor(editor); // Save - return editor.save(options); + return editor.save(groupId, options); })); // Editors to save sequentially @@ -700,7 +702,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { continue; // might have been disposed from from the save already } - const result = await editor.saveAs(groupId, options); + const result = options?.saveAs ? await editor.saveAs(groupId, options) : await editor.save(groupId, options); if (!result) { return false; // failed or cancelled, abort } @@ -710,35 +712,27 @@ export class EditorService extends Disposable implements EditorServiceImpl { } saveAll(options?: ISaveAllEditorsOptions): Promise { + return this.save(this.getSaveableEditors(!!options?.includeUntitled), options); + } + + async revertAll(options?: IRevertOptions): Promise { + const result = await Promise.all(this.getSaveableEditors(true /* include untitled */).map(async ({ editor }) => editor.revert(options))); + + return result.every(success => !!success); + } + + private getSaveableEditors(includeUntitled: boolean): IEditorIdentifier[] { const editors: IEditorIdentifier[] = []; - // Collect all editors in MRU order that are dirty - this.forEachDirtyEditor(({ groupId, editor }) => { - if (!editor.isUntitled() || options?.includeUntitled) { - editors.push({ groupId, editor }); - } - }); - - return this.save(editors, options); - } - - async revertAll(options?: IRevertOptions): Promise { - - // Revert each editor in MRU order - const reverts: Promise[] = []; - this.forEachDirtyEditor(({ editor }) => reverts.push(editor.revert(options))); - - await Promise.all(reverts); - } - - private forEachDirtyEditor(callback: (editor: IEditorIdentifier) => void): void { for (const group of this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { - if (editor.isDirty()) { - callback({ groupId: group.id, editor }); + if (editor.isDirty() && !editor.isReadonly() && (!editor.isUntitled() || includeUntitled)) { + editors.push({ groupId: group.id, editor }); } } } + + return editors; } //#endregion diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 2fb95f9f8ec..6bb7691e0b6 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -5,12 +5,11 @@ import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IResourceInput, IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; -import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IEditorIdentifier } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IEditorIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { Event } from 'vs/base/common/event'; import { IEditor as ICodeEditor } from 'vs/editor/common/editorCommon'; import { IEditorGroup, IEditorReplacement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { ISaveOptions, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export const IEditorService = createDecorator('editorService'); @@ -201,7 +200,7 @@ export interface IEditorService { /** * Converts a lightweight input to a workbench editor input. */ - createInput(input: IResourceEditor): IEditorInput | null; + createInput(input: IResourceEditor): IEditorInput; /** * Save the provided list of editors. @@ -216,5 +215,5 @@ export interface IEditorService { /** * Reverts all editors. */ - revertAll(options?: IRevertOptions): Promise; + revertAll(options?: IRevertOptions): Promise; } diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index ad798f5ce43..a6e1c34e790 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { IEditorModel, EditorActivation } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorInput, EditorOptions, IFileEditorInput, IEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IFileEditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; @@ -50,6 +50,10 @@ export class TestEditorControl extends BaseEditor { export class TestEditorInput extends EditorInput implements IFileEditorInput { public gotDisposed = false; + public gotSaved = false; + public gotSavedAs = false; + public gotReverted = false; + public dirty = false; private fails = false; constructor(private resource: URI) { super(); } @@ -66,6 +70,26 @@ export class TestEditorInput extends EditorInput implements IFileEditorInput { setFailToOpen(): void { this.fails = true; } + save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + this.gotSaved = true; + return Promise.resolve(true); + } + saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + this.gotSavedAs = true; + return Promise.resolve(true); + } + revert(options?: IRevertOptions): Promise { + this.gotReverted = true; + this.gotSaved = false; + this.gotSavedAs = false; + return Promise.resolve(true); + } + isDirty(): boolean { + return this.dirty; + } + isReadonly(): boolean { + return false; + } dispose(): void { super.dispose(); this.gotDisposed = true; @@ -686,4 +710,45 @@ suite('EditorService', () => { let failingEditor = await service.openEditor(failingInput); assert.ok(!failingEditor); }); + + test('save, saveAll, revertAll', async function () { + const partInstantiator = workbenchInstantiationService(); + + const part = partInstantiator.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); + + const service: IEditorService = testInstantiationService.createInstance(EditorService); + + const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside')); + input1.dirty = true; + const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside')); + input2.dirty = true; + + const rootGroup = part.activeGroup; + + await part.whenRestored; + + await service.openEditor(input1, { pinned: true }); + await service.openEditor(input2, { pinned: true }); + + await service.save({ groupId: rootGroup.id, editor: input1 }); + assert.equal(input1.gotSaved, true); + + await service.save({ groupId: rootGroup.id, editor: input1 }, { saveAs: true }); + assert.equal(input1.gotSavedAs, true); + + await service.revertAll(); + assert.equal(input1.gotReverted, true); + + await service.saveAll(); + assert.equal(input1.gotSaved, true); + assert.equal(input2.gotSaved, true); + + await service.saveAll({ saveAs: true }); + assert.equal(input1.gotSavedAs, true); + assert.equal(input2.gotSavedAs, true); + }); }); diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 4d75a95a8e4..400006f193f 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -9,7 +9,7 @@ import { Emitter, AsyncEmitter } from 'vs/base/common/event'; import * as platform from 'vs/base/common/platform'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; -import { SaveReason, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { SaveReason, IRevertOptions } from 'vs/workbench/common/editor'; import { ILifecycleService, ShutdownReason, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IFileService, FileOperationError, FileOperationResult, HotExitConfiguration, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files'; diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index f18c70d2aee..f125c5235e5 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -12,7 +12,7 @@ import { isUndefinedOrNull, assertIsDefined } from 'vs/base/common/types'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITextFileService, ModelState, ITextFileEditorModel, ISaveErrorHandler, ISaveParticipant, StateChange, ITextFileStreamContent, ILoadOptions, LoadReason, IResolvedTextFileEditorModel, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; -import { EncodingMode } from 'vs/workbench/common/editor'; +import { EncodingMode, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IFileService, FileOperationError, FileOperationResult, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED } from 'vs/platform/files/common/files'; @@ -29,7 +29,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { isEqual, isEqualOrParent, extname, basename, joinPath } from 'vs/base/common/resources'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Schemas } from 'vs/base/common/network'; -import { IWorkingCopyService, WorkingCopyCapabilities, SaveReason, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFilesConfigurationService, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; export interface IBackupMetaData { diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index b29d7dc560d..51338221b40 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -6,7 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { Event, IWaitUntil } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IEncodingSupport, IModeSupport } from 'vs/workbench/common/editor'; +import { IEncodingSupport, IModeSupport, ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult, FileOperation } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; @@ -14,7 +14,7 @@ import { ITextBufferFactory, ITextModel, ITextSnapshot } from 'vs/editor/common/ import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { isNative } from 'vs/base/common/platform'; -import { IWorkingCopy, ISaveOptions, SaveReason, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export const ITextFileService = createDecorator('textFileService'); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index ecac6c3a877..fd4da1c641f 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -19,68 +19,6 @@ export const enum WorkingCopyCapabilities { AutoSave = 1 << 1 } -export const enum SaveReason { - - /** - * Explicit user gesture. - */ - EXPLICIT = 1, - - /** - * Auto save after a timeout. - */ - AUTO = 2, - - /** - * Auto save after editor focus change. - */ - FOCUS_CHANGE = 3, - - /** - * Auto save after window change. - */ - WINDOW_CHANGE = 4 -} - -export interface ISaveOptions { - - /** - * An indicator how the save operation was triggered. - */ - reason?: SaveReason; - - /** - * Forces to load the contents of the working copy - * again even if the working copy is not dirty. - */ - force?: boolean; - - /** - * Instructs the save operation to skip any save participants. - */ - skipSaveParticipants?: boolean; - - /** - * A hint as to which file systems should be available for saving. - */ - availableFileSystems?: string[]; -} - -export interface IRevertOptions { - - /** - * Forces to load the contents of the working copy - * again even if the working copy is not dirty. - */ - force?: boolean; - - /** - * A soft revert will clear dirty state of a working copy - * but will not attempt to load it from its persisted state. - */ - soft?: boolean; -} - export interface IWorkingCopy { //#region Dirty Tracking @@ -92,15 +30,6 @@ export interface IWorkingCopy { //#endregion - //#region Save/Revert - - save(options?: ISaveOptions): Promise; - - revert(options?: IRevertOptions): Promise; - - //#endregion - - readonly resource: URI; readonly capabilities: WorkingCopyCapabilities; diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index f9fe1649555..7110cd28582 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IWorkingCopy, ISaveOptions, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -41,9 +41,6 @@ suite('WorkingCopyService', () => { return this.dirty; } - async save(options?: ISaveOptions): Promise { return true; } - async revert(options?: IRevertOptions): Promise { return true; } - dispose(): void { this._onDispose.fire(); diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index 385bf0f57a8..fa7b43c3a9a 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService, rewriteWorkspaceFileForNewLocation, WORKSPACE_FILTER, IEnterWorkspaceResult } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService, rewriteWorkspaceFileForNewLocation, WORKSPACE_FILTER, IEnterWorkspaceResult, hasWorkspaceFileExtension, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { ConfigurationScope, IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -43,13 +43,25 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi @IHostService protected readonly hostService: IHostService ) { } - pickNewWorkspacePath(): Promise { - return this.fileDialogService.showSaveDialog({ + async pickNewWorkspacePath(): Promise { + let workspacePath = await this.fileDialogService.showSaveDialog({ saveLabel: mnemonicButtonLabel(nls.localize('save', "Save")), title: nls.localize('saveWorkspace', "Save Workspace"), filters: WORKSPACE_FILTER, defaultUri: this.fileDialogService.defaultWorkspacePath() }); + + if (!workspacePath) { + return; // canceled + } + + if (!hasWorkspaceFileExtension(workspacePath)) { + // Always ensure we have workspace file extension + // (see https://github.com/microsoft/vscode/issues/84818) + workspacePath = workspacePath.with({ path: `${workspacePath.path}.${WORKSPACE_EXTENSION}` }); + } + + return workspacePath; } updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise { diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts index 0e4d2acb7bd..313272d338f 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts @@ -10,7 +10,7 @@ import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from 'vs/workbe import { MainThreadTextEditorsShape, IWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/common/extHostDocumentSaveParticipant'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; -import { SaveReason } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { SaveReason } from 'vs/workbench/common/editor'; import * as vscode from 'vscode'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts index c6122bedf48..40b3489c80f 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts @@ -14,7 +14,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { ITextFileService, IResolvedTextFileEditorModel, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; -import { SaveReason } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { SaveReason } from 'vs/workbench/common/editor'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; class ServiceAccessor { diff --git a/src/vs/workbench/test/electron-browser/api/semanticTokens.test.ts b/src/vs/workbench/test/electron-browser/api/semanticTokens.test.ts new file mode 100644 index 00000000000..978888ef532 --- /dev/null +++ b/src/vs/workbench/test/electron-browser/api/semanticTokens.test.ts @@ -0,0 +1,343 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import * as types from 'vs/workbench/api/common/extHostTypes'; +import { TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; +import { SemanticColoringAdapter, SemanticColoringConstants } from 'vs/workbench/api/common/extHostLanguageFeatures'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; +import * as vscode from 'vscode'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { decodeSemanticTokensDto, ISemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokens'; +import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; + +suite('SemanticColoringAdapter', () => { + + const resource = URI.parse('foo:bar'); + const rpcProtocol = new TestRPCProtocol(); + + const initialText = [ + 'const enum E01 {}', + 'const enum E02 {}', + 'const enum E03 {}', + 'const enum E04 {}', + 'const enum E05 {}', + 'const enum E06 {}', + 'const enum E07 {}', + 'const enum E08 {}', + 'const enum E09 {}', + 'const enum E10 {}', + 'const enum E11 {}', + 'const enum E12 {}', + 'const enum E13 {}', + 'const enum E14 {}', + 'const enum E15 {}', + 'const enum E16 {}', + 'const enum E17 {}', + 'const enum E18 {}', + 'const enum E19 {}', + 'const enum E20 {}', + 'const enum E21 {}', + 'const enum E22 {}', + 'const enum E23 {}', + ].join('\n'); + + const extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol); + extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ + addedDocuments: [{ + isDirty: false, + versionId: 1, + modeId: 'javascript', + uri: resource, + lines: initialText.split(/\n/), + EOL: '\n', + }] + }); + const extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); + rpcProtocol.set(ExtHostContext.ExtHostDocuments, extHostDocuments); + + const semanticTokensProvider = new class implements vscode.SemanticColoringProvider { + provideSemanticColoring(document: vscode.TextDocument, token: vscode.CancellationToken): types.SemanticColoring { + const lines = document.getText().split(/\r\n|\r|\n/g); + const tokens: number[] = []; + const pushToken = (line: number, startCharacter: number, endCharacter: number, type: number) => { + tokens.push(line); + tokens.push(startCharacter); + tokens.push(endCharacter); + tokens.push(type); + tokens.push(0); + }; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const m = line.match(/^(const enum )([\w\d]+) \{\}/); + if (m) { + pushToken(i, m[1].length, m[1].length + m[2].length, parseInt(m[2].substr(1))); + } + } + return new types.SemanticColoring([new types.SemanticColoringArea(0, new Uint32Array(tokens))]); + } + }; + + let adapter: SemanticColoringAdapter; + let doc: ExtHostDocumentData; + + setup(() => { + adapter = new SemanticColoringAdapter(extHostDocuments, semanticTokensProvider, 10, SemanticColoringConstants.DesiredMaxAreas, 5); + doc = extHostDocumentsAndEditors.getDocument(resource)!; + const docLineCount = doc.document.lineCount; + const allRange = { startLineNumber: 1, startColumn: 1, endLineNumber: docLineCount, endColumn: doc.document.lineAt(docLineCount - 1).text.length + 1 }; + doc.onEvents({ + versionId: 1, + eol: '\n', + changes: [{ + range: allRange, + rangeOffset: 0, + rangeLength: 0, + text: initialText + }] + }); + }); + + type SimpleTokensDto = { type: 'full'; line: number; tokens: number[]; } | { type: 'delta'; line: number; oldIndex: number }; + + function assertDTO(actual: ISemanticTokensDto, expected: SimpleTokensDto[]): void { + const simpleActual: SimpleTokensDto[] = actual.areas.map((area) => { + if (area.type === 'full') { + const tokenCount = (area.data.length / 5) | 0; + let tokens: number[] = []; + for (let i = 0; i < tokenCount; i++) { + tokens.push(area.data[5 * i]); + } + return { + type: 'full', + line: area.line, + tokens: tokens + }; + } + return { + type: 'delta', + line: area.line, + oldIndex: area.oldIndex + }; + }); + assert.deepEqual(simpleActual, expected); + } + + test('single area - breaks it up', async () => { + const dto = (await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!; + const result = decodeSemanticTokensDto(dto); + assertDTO(result, [ + { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 21, tokens: [0, 1, 2] }, + ]); + }); + + test('single area - after a not important change', async () => { + const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); + assertDTO(result1, [ + { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 21, tokens: [0, 1, 2] }, + ]); + + doc.onEvents({ + versionId: 2, + eol: '\n', + changes: [{ + range: { startLineNumber: 2, startColumn: 18, endLineNumber: 2, endColumn: 18 }, + rangeOffset: 0, + rangeLength: 0, + text: '//' + }] + }); + + const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); + assertDTO(result2, [ + { type: 'delta', line: 1, oldIndex: 0 }, + { type: 'delta', line: 11, oldIndex: 1 }, + { type: 'delta', line: 21, oldIndex: 2 }, + ]); + adapter.releaseSemanticColoring(result1.id); + }); + + test('single area - after a single removal in the first block', async () => { + const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); + assertDTO(result1, [ + { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 21, tokens: [0, 1, 2] }, + ]); + + doc.onEvents({ + versionId: 2, + eol: '\n', + changes: [{ + range: { startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }, + rangeOffset: 0, + rangeLength: 0, + text: '//' + }] + }); + + const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); + assertDTO(result2, [ + { type: 'full', line: 1, tokens: [0, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'delta', line: 11, oldIndex: 1 }, + { type: 'delta', line: 21, oldIndex: 2 }, + ]); + adapter.releaseSemanticColoring(result1.id); + }); + + test('single area - after a not important change', async () => { + const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); + assertDTO(result1, [ + { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 21, tokens: [0, 1, 2] }, + ]); + + doc.onEvents({ + versionId: 2, + eol: '\n', + changes: [{ + range: { startLineNumber: 2, startColumn: 18, endLineNumber: 2, endColumn: 18 }, + rangeOffset: 0, + rangeLength: 0, + text: '//' + }] + }); + + const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); + assertDTO(result2, [ + { type: 'delta', line: 1, oldIndex: 0 }, + { type: 'delta', line: 11, oldIndex: 1 }, + { type: 'delta', line: 21, oldIndex: 2 }, + ]); + adapter.releaseSemanticColoring(result1.id); + }); + + test('single area - after a down shift of all the blocks', async () => { + const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); + assertDTO(result1, [ + { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 21, tokens: [0, 1, 2] }, + ]); + + doc.onEvents({ + versionId: 2, + eol: '\n', + changes: [{ + range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, + rangeOffset: 0, + rangeLength: 0, + text: '\n' + }] + }); + + const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); + assertDTO(result2, [ + { type: 'delta', line: 2, oldIndex: 0 }, + { type: 'delta', line: 12, oldIndex: 1 }, + { type: 'delta', line: 22, oldIndex: 2 }, + ]); + adapter.releaseSemanticColoring(result1.id); + }); + + test('single area - after a single removal in the last block', async () => { + const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); + assertDTO(result1, [ + { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 21, tokens: [0, 1, 2] }, + ]); + + doc.onEvents({ + versionId: 2, + eol: '\n', + changes: [{ + range: { startLineNumber: 22, startColumn: 1, endLineNumber: 22, endColumn: 1 }, + rangeOffset: 0, + rangeLength: 0, + text: '//' + }] + }); + + const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); + assertDTO(result2, [ + { type: 'delta', line: 1, oldIndex: 0 }, + { type: 'delta', line: 11, oldIndex: 1 }, + { type: 'full', line: 21, tokens: [0, 2] }, + ]); + adapter.releaseSemanticColoring(result1.id); + }); + + test('single area - after a single addition in the first block', async () => { + const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); + assertDTO(result1, [ + { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { type: 'full', line: 21, tokens: [0, 1, 2] }, + ]); + + doc.onEvents({ + versionId: 2, + eol: '\n', + changes: [{ + range: { startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }, + rangeOffset: 0, + rangeLength: 0, + text: 'const enum E00 {}\n' + }] + }); + + const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); + assertDTO(result2, [ + { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }, + { type: 'delta', line: 12, oldIndex: 1 }, + { type: 'delta', line: 22, oldIndex: 2 }, + ]); + adapter.releaseSemanticColoring(result1.id); + }); + + test('going from empty to 1 semantic token', async () => { + doc.onEvents({ + versionId: 2, + eol: '\n', + changes: [{ + range: { startLineNumber: 1, startColumn: 1, endLineNumber: 23, endColumn: 18 }, + rangeOffset: 0, + rangeLength: 0, + text: '' + }] + }); + + const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); + assertDTO(result1, [ + { type: 'full', line: 1, tokens: [] }, + ]); + + doc.onEvents({ + versionId: 3, + eol: '\n', + changes: [{ + range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, + rangeOffset: 0, + rangeLength: 0, + text: 'const enum E01 {}\n' + }] + }); + + const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); + assertDTO(result2, [ + { type: 'full', line: 1, tokens: [0] } + ]); + adapter.releaseSemanticColoring(result1.id); + }); +}); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 6268b95feee..861d7bc9a7c 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -11,7 +11,7 @@ import * as resources from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions } from 'vs/workbench/common/editor'; +import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; @@ -92,7 +92,7 @@ import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/ele import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { find } from 'vs/base/common/arrays'; -import { WorkingCopyService, IWorkingCopyService, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { WorkingCopyService, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFilesConfigurationService, FilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { @@ -930,7 +930,7 @@ export class TestEditorService implements EditorServiceImpl { throw new Error('Method not implemented.'); } - revertAll(options?: IRevertOptions): Promise { + revertAll(options?: IRevertOptions): Promise { throw new Error('Method not implemented.'); } }