Merge branch 'main' into dev/dmitriv/drag-and-drop-profile

This commit is contained in:
Dmitriy Vasyura 2025-12-27 13:08:27 -08:00 committed by GitHub
commit 4e51d2834e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 341 additions and 146 deletions

View File

@ -7,7 +7,7 @@
import { Model } from '../model';
import { Repository as BaseRepository, Resource } from '../repository';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider, GitErrorCodes, CloneOptions, CommitShortStat, DiffChange } from './git';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider, GitErrorCodes, CloneOptions, CommitShortStat, DiffChange, Worktree } from './git';
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode';
import { combinedDisposable, filterEvent, mapEvent } from '../util';
import { toGitUri } from '../uri';
@ -52,6 +52,7 @@ export class ApiRepositoryState implements RepositoryState {
get refs(): Ref[] { console.warn('Deprecated. Use ApiRepository.getRefs() instead.'); return []; }
get remotes(): Remote[] { return [...this.#repository.remotes]; }
get submodules(): Submodule[] { return [...this.#repository.submodules]; }
get worktrees(): Worktree[] { return this.#repository.worktrees; }
get rebaseCommit(): Commit | undefined { return this.#repository.rebaseCommit; }
get mergeChanges(): Change[] { return this.#repository.mergeGroup.resourceStates.map(r => new ApiChange(r)); }
@ -553,6 +554,7 @@ export function registerAPICommands(extension: GitExtensionImpl): Disposable {
refs: state.refs.map(ref),
remotes: state.remotes,
submodules: state.submodules,
worktrees: state.worktrees,
rebaseCommit: state.rebaseCommit,
mergeChanges: state.mergeChanges.map(change),
indexChanges: state.indexChanges.map(change),

View File

@ -81,7 +81,6 @@ export interface Worktree {
readonly path: string;
readonly ref: string;
readonly detached: boolean;
readonly commitDetails?: Commit;
}
export const enum Status {
@ -131,6 +130,7 @@ export interface RepositoryState {
readonly refs: Ref[];
readonly remotes: Remote[];
readonly submodules: Submodule[];
readonly worktrees: Worktree[];
readonly rebaseCommit: Commit | undefined;
readonly mergeChanges: Change[];
@ -262,7 +262,7 @@ export interface Repository {
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise<DiffChange[]>;
hashObject(data: string): Promise<string>;
createBranch(name: string, checkout: boolean, ref?: string): Promise<void>;

View File

@ -8,8 +8,8 @@ import * as path from 'path';
import { Command, commands, Disposable, MessageOptions, Position, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages, SourceControlArtifact } from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator';
import { ForcePushMode, GitErrorCodes, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote, Branch, Ref, Worktree } from './api/git';
import { Git, GitError, Stash } from './git';
import { ForcePushMode, GitErrorCodes, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote, Branch, Ref } from './api/git';
import { Git, GitError, Stash, Worktree } from './git';
import { Model } from './model';
import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository';
import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges, compareLineChanges } from './staging';

View File

@ -13,7 +13,7 @@ import { EventEmitter } from 'events';
import * as filetype from 'file-type';
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant, relativePathWithNoFallback, Mutable } from './util';
import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode';
import { Commit as ApiCommit, Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery as ApiRefQuery, InitOptions, Worktree, DiffChange } from './api/git';
import { Commit as ApiCommit, Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery as ApiRefQuery, InitOptions, DiffChange, Worktree as ApiWorktree } from './api/git';
import * as byline from 'byline';
import { StringDecoder } from 'string_decoder';
@ -1303,6 +1303,10 @@ export interface PullOptions {
readonly cancellationToken?: CancellationToken;
}
export interface Worktree extends ApiWorktree {
readonly commitDetails?: ApiCommit;
}
export class Repository {
private _isUsingRefTable = false;

View File

@ -457,7 +457,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi
@debounce(500)
private eventuallyScanPossibleGitRepositories(): void {
for (const path of this.possibleGitRepositoryPaths) {
this.openRepository(path, false, true);
this.openRepository(path);
}
this.possibleGitRepositoryPaths.clear();
@ -572,7 +572,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi
}
@sequentialize
async openRepository(repoPath: string, openIfClosed = false, openIfParent = false): Promise<void> {
async openRepository(repoPath: string, openIfClosed = false): Promise<void> {
this.logger.trace(`[Model][openRepository] Repository: ${repoPath}`);
const existingRepository = await this.getRepositoryExact(repoPath);
if (existingRepository) {
@ -621,7 +621,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi
const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt');
if (parentRepositoryConfig !== 'always' && this.globalState.get<boolean>(`parentRepository:${repositoryRoot}`) !== true) {
const isRepositoryOutsideWorkspace = await this.isRepositoryOutsideWorkspace(repositoryRoot);
if (!openIfParent && isRepositoryOutsideWorkspace) {
if (isRepositoryOutsideWorkspace) {
this.logger.trace(`[Model][openRepository] Repository in parent folder: ${repositoryRoot}`);
if (!this._parentRepositoriesManager.hasRepository(repositoryRoot)) {
@ -1094,6 +1094,14 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi
return true;
}
// The repository path may be a worktree (usually stored outside the workspace) so we have
// to check the repository path against all the worktree paths of the repositories that have
// already been opened.
const worktreePaths = this.repositories.map(r => r.worktrees.map(w => w.path)).flat();
if (worktreePaths.some(p => pathEquals(p, repositoryPath))) {
return false;
}
// The repository path may be a canonical path or it may contain a symbolic link so we have
// to match it against the workspace folders and the canonical paths of the workspace folders
const workspaceFolderPaths = new Set<string | undefined>([

View File

@ -10,11 +10,11 @@ import picomatch from 'picomatch';
import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, FileType, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode';
import { ActionButton } from './actionButton';
import { ApiRepository } from './api/api1';
import { Branch, BranchQuery, Change, CommitOptions, DiffChange, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, Worktree } from './api/git';
import { Branch, BranchQuery, Change, CommitOptions, DiffChange, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status } from './api/git';
import { AutoFetcher } from './autofetch';
import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection';
import { debounce, memoize, sequentialize, throttle } from './decorators';
import { Repository as BaseRepository, BlameInformation, Commit, CommitShortStat, GitError, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule } from './git';
import { Repository as BaseRepository, BlameInformation, Commit, CommitShortStat, GitError, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule, Worktree } from './git';
import { GitHistoryProvider } from './historyProvider';
import { Operation, OperationKind, OperationManager, OperationResult } from './operation';
import { CommitCommandsCenter, IPostCommitCommandsProviderRegistry } from './postCommitCommands';

View File

@ -25,5 +25,40 @@
"start": "^\\s*#Region\\b",
"end": "^\\s*#End Region\\b"
}
}
},
"indentationRules": {
"decreaseIndentPattern": {
"pattern": "^\\s*((End\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Else|ElseIf|Case|Catch|Finally|Loop|Next|Wend|Until)\\b",
"flags": "i"
},
"increaseIndentPattern": {
"pattern": "^\\s*((If|ElseIf).*Then(?!\\s+(End\\s+If))\\s*(('|REM).*)?$)|\\b(Else|While|For|Do|Select\\s+Case|Case|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Try|Catch|Finally|SyncLock|Using|Property|Get|Set|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b(?!.*\\bEnd\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b).*(('|REM).*)?$",
"flags": "i"
}
},
"onEnterRules": [
// Prevent indent after End statements, block terminators (Else, ElseIf, Loop, Next, etc.)
{
"beforeText": { "pattern": "^\\s*((End\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Else|ElseIf|Loop|Next|Wend|Until)\\b.*$", "flags": "i" },
"action": {
"indent": "none"
}
},
// Prevent indent when pressing Enter on a blank line after End statements or block terminators
{
"beforeText": "^\\s*$",
"previousLineText": { "pattern": "^\\s*((End\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Else|ElseIf|Loop|Next|Wend|Until)\\b.*$", "flags": "i" },
"action": {
"indent": "none"
}
},
// Prevent indent after lines ending with closing parenthesis (e.g., function calls, method invocations)
{
"beforeText": { "pattern": "^[^'\"]*\\)\\s*('.*)?$", "flags": "i" },
"afterText": "^(?!\\s*\\))",
"action": {
"indent": "none"
}
}
]
}

View File

@ -364,6 +364,9 @@ export class Range {
return new Range(this.startLineNumber + lineCount, this.startColumn, this.endLineNumber + lineCount, this.endColumn);
}
/**
* Test if this range starts and ends on the same line.
*/
public isSingleLine(): boolean {
return this.startLineNumber === this.endLineNumber;
}

View File

@ -192,10 +192,7 @@ export function guessIndentation(source: ITextBuffer, defaultTabSize: number, de
// Guess tabSize only if inserting spaces...
if (insertSpaces) {
let tabSizeScore = (insertSpaces ? 0 : 0.1 * linesCount);
// console.log("score threshold: " + tabSizeScore);
let tabSizeScore = 0;
ALLOWED_TAB_SIZE_GUESSES.forEach((possibleTabSize) => {
const possibleTabSizeScore = spacesDiffCount[possibleTabSize];
if (possibleTabSizeScore > tabSizeScore) {
@ -204,14 +201,14 @@ export function guessIndentation(source: ITextBuffer, defaultTabSize: number, de
}
});
// Let a tabSize of 2 win even if it is not the maximum
// (only in case 4 was guessed)
if (tabSize === 4 && spacesDiffCount[4] > 0 && spacesDiffCount[2] > 0 && spacesDiffCount[2] >= spacesDiffCount[4] / 2) {
// Let a tabSize of 2 win over 4 only if it has at least 2/3 of the occurrences of 4
// This helps detect 2-space indentation in cases like YAML files where there might be
// some 4-space diffs from deeper nesting, while still preferring 4 when it's clearly predominant
if (tabSize === 4 && spacesDiffCount[4] > 0 && spacesDiffCount[2] > 0 && spacesDiffCount[2] >= spacesDiffCount[4] * 2 / 3) {
tabSize = 2;
}
}
// console.log('--------------------------');
// console.log('linesIndentedWithTabsCount: ' + linesIndentedWithTabsCount + ', linesIndentedWithSpacesCount: ' + linesIndentedWithSpacesCount);
// console.log('spacesDiffCount: ' + spacesDiffCount);

View File

@ -18,7 +18,7 @@ import { NullState } from '../../../../common/languages/nullTokenize.js';
import { AutoIndentOnPaste, IndentationToSpacesCommand, IndentationToTabsCommand } from '../../browser/indentation.js';
import { withTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
import { testCommand } from '../../../../test/browser/testCommand.js';
import { goIndentationRules, htmlIndentationRules, javascriptIndentationRules, latexIndentationRules, luaIndentationRules, phpIndentationRules, rubyIndentationRules } from '../../../../test/common/modes/supports/indentationRules.js';
import { goIndentationRules, htmlIndentationRules, javascriptIndentationRules, latexIndentationRules, luaIndentationRules, phpIndentationRules, rubyIndentationRules, vbIndentationRules } from '../../../../test/common/modes/supports/indentationRules.js';
import { cppOnEnterRules, htmlOnEnterRules, javascriptOnEnterRules, phpOnEnterRules } from '../../../../test/common/modes/supports/onEnterRules.js';
import { TypeOperations } from '../../../../common/cursor/cursorTypeOperations.js';
import { cppBracketRules, goBracketRules, htmlBracketRules, latexBracketRules, luaBracketRules, phpBracketRules, rubyBracketRules, typescriptBracketRules, vbBracketRules } from '../../../../test/common/modes/supports/bracketRules.js';
@ -94,6 +94,7 @@ export function registerLanguageConfiguration(languageConfigurationService: ILan
case Language.VB:
return languageConfigurationService.register(language, {
brackets: vbBracketRules,
indentationRules: vbIndentationRules,
});
case Language.Latex:
return languageConfigurationService.register(language, {
@ -1737,14 +1738,14 @@ suite('Auto Indent On Type - Visual Basic', () => {
assert.ok(true);
});
test.skip('issue #118932: no indentation in visual basic files', () => {
test('issue #118932: no indentation in visual basic files', () => {
// https://github.com/microsoft/vscode/issues/118932
const model = createTextModel([
'if True then',
'If True Then',
' Some code',
' end i',
' End I',
].join('\n'), languageId, {});
disposables.add(model);
@ -1752,9 +1753,9 @@ suite('Auto Indent On Type - Visual Basic', () => {
editor.setSelection(new Selection(3, 10, 3, 10));
viewModel.type('f', 'keyboard');
assert.strictEqual(model.getValue(), [
'if True then',
'If True Then',
' Some code',
'end if',
'End If',
].join('\n'));
});
});

View File

@ -251,7 +251,7 @@ export abstract class AbstractSortLinesAction extends EditorAction {
const model = editor.getModel();
let selections = editor.getSelections();
if (selections.length === 1 && selections[0].isEmpty()) {
if (selections.length === 1 && selections[0].isSingleLine()) {
// Apply to whole document.
selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];
}
@ -322,7 +322,7 @@ export class DeleteDuplicateLinesAction extends EditorAction {
let updateSelection = true;
let selections = editor.getSelections();
if (selections.length === 1 && selections[0].isEmpty()) {
if (selections.length === 1 && selections[0].isSingleLine()) {
// Apply to whole document.
selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];
updateSelection = false;
@ -389,7 +389,7 @@ export class ReverseLinesAction extends EditorAction {
const model: ITextModel = editor.getModel();
const originalSelections = editor.getSelections();
let selections = originalSelections;
if (selections.length === 1 && selections[0].isEmpty()) {
if (selections.length === 1 && selections[0].isSingleLine()) {
// Apply to whole document.
selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];
}

View File

@ -106,6 +106,26 @@ suite('Editor Contrib - Line Operations', () => {
});
});
});
test('applies to whole document when selection is single line', function () {
withTestCodeEditor(
[
'omicron',
'beta',
'alpha'
], {}, (editor) => {
const model = editor.getModel()!;
const sortLinesAscendingAction = new SortLinesAscendingAction();
editor.setSelection(new Selection(2, 1, 2, 4));
executeAction(sortLinesAscendingAction, editor);
assert.deepStrictEqual(model.getLinesContent(), [
'alpha',
'beta',
'omicron'
]);
});
});
});
suite('SortLinesDescendingAction', () => {
@ -248,6 +268,23 @@ suite('Editor Contrib - Line Operations', () => {
});
});
});
test('applies to whole document when selection is single line', function () {
withTestCodeEditor(
[
'alpha',
'beta',
'alpha',
'omicron'
], {}, (editor) => {
const model = editor.getModel()!;
const deleteDuplicateLinesAction = new DeleteDuplicateLinesAction();
editor.setSelection(new Selection(2, 1, 2, 2));
executeAction(deleteDuplicateLinesAction, editor);
assert.deepStrictEqual(model.getLinesContent(), ['alpha', 'beta', 'omicron']);
});
});
});
@ -729,7 +766,7 @@ suite('Editor Contrib - Line Operations', () => {
});
});
test('handles single line selection', function () {
test('applies to whole document when selection is single line', function () {
withTestCodeEditor(
[
'line1',
@ -742,8 +779,7 @@ suite('Editor Contrib - Line Operations', () => {
// Select only line 2
editor.setSelection(new Selection(2, 1, 2, 6));
executeAction(reverseLinesAction, editor);
// Single line should remain unchanged
assert.deepStrictEqual(model.getLinesContent(), ['line1', 'line2', 'line3']);
assert.deepStrictEqual(model.getLinesContent(), ['line3', 'line2', 'line1']);
});
});
@ -765,6 +801,26 @@ suite('Editor Contrib - Line Operations', () => {
assert.deepStrictEqual(model.getLinesContent(), ['line1', 'line3', 'line2', 'line4', 'line5']);
});
});
test('applies to whole document when selection is single line', function () {
withTestCodeEditor(
[
'omicron',
'beta',
'alpha'
], {}, (editor) => {
const model = editor.getModel()!;
const reverseLinesAction = new ReverseLinesAction();
editor.setSelection(new Selection(2, 1, 2, 4));
executeAction(reverseLinesAction, editor);
assert.deepStrictEqual(model.getLinesContent(), [
'alpha',
'beta',
'omicron'
]);
});
});
});
test('transpose', () => {

View File

@ -60,7 +60,6 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
private readonly _editor: ICodeEditor;
private _state: StickyScrollWidgetState | undefined;
private _lineHeight: number;
private _renderedStickyLines: RenderedStickyLine[] = [];
private _lineNumbers: number[] = [];
private _lastLineRelativePosition: number = 0;
@ -79,7 +78,6 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
super();
this._editor = editor;
this._lineHeight = editor.getOption(EditorOption.lineHeight);
this._lineNumbersDomNode.className = 'sticky-widget-line-numbers';
this._lineNumbersDomNode.setAttribute('role', 'none');
@ -102,9 +100,6 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
if (e.hasChanged(EditorOption.stickyScroll)) {
updateScrollLeftPosition();
}
if (e.hasChanged(EditorOption.lineHeight)) {
this._lineHeight = this._editor.getOption(EditorOption.lineHeight);
}
}));
this._register(this._editor.onDidScrollChange((e) => {
if (e.scrollLeftChanged) {
@ -296,92 +291,15 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
}
private _renderChildNode(viewModel: IViewModel, index: number, line: number, top: number, isLastLine: boolean, foldingModel: FoldingModel | undefined, layoutInfo: EditorLayoutInfo): RenderedStickyLine {
const viewLineNumber = viewModel.coordinatesConverter.convertModelPositionToViewPosition(new Position(line, 1)).lineNumber;
const lineRenderingData = viewModel.getViewLineRenderingData(viewLineNumber);
const lineNumberOption = this._editor.getOption(EditorOption.lineNumbers);
const verticalScrollbarSize = this._editor.getOption(EditorOption.scrollbar).verticalScrollbarSize;
let actualInlineDecorations: LineDecoration[];
try {
actualInlineDecorations = LineDecoration.filter(lineRenderingData.inlineDecorations, viewLineNumber, lineRenderingData.minColumn, lineRenderingData.maxColumn);
} catch (err) {
actualInlineDecorations = [];
}
const lineHeight = this._editor.getLineHeightForPosition(new Position(line, 1));
const textDirection = viewModel.getTextDirection(line);
const renderLineInput: RenderLineInput = new RenderLineInput(true, true, lineRenderingData.content,
lineRenderingData.continuesWithWrappedLine,
lineRenderingData.isBasicASCII, lineRenderingData.containsRTL, 0,
lineRenderingData.tokens, actualInlineDecorations,
lineRenderingData.tabSize, lineRenderingData.startVisibleColumn,
1, 1, 1, 500, 'none', true, true, null,
textDirection, verticalScrollbarSize
);
const sb = new StringBuilder(2000);
const renderOutput = renderViewLine(renderLineInput, sb);
let newLine;
if (_ttPolicy) {
newLine = _ttPolicy.createHTML(sb.build());
} else {
newLine = sb.build();
}
const lineHTMLNode = document.createElement('span');
lineHTMLNode.setAttribute(STICKY_INDEX_ATTR, String(index));
lineHTMLNode.setAttribute(STICKY_IS_LINE_ATTR, '');
lineHTMLNode.setAttribute('role', 'listitem');
lineHTMLNode.tabIndex = 0;
lineHTMLNode.className = 'sticky-line-content';
lineHTMLNode.classList.add(`stickyLine${line}`);
lineHTMLNode.style.lineHeight = `${lineHeight}px`;
lineHTMLNode.innerHTML = newLine as string;
const lineNumberHTMLNode = document.createElement('span');
lineNumberHTMLNode.setAttribute(STICKY_INDEX_ATTR, String(index));
lineNumberHTMLNode.setAttribute(STICKY_IS_LINE_NUMBER_ATTR, '');
lineNumberHTMLNode.className = 'sticky-line-number';
lineNumberHTMLNode.style.lineHeight = `${lineHeight}px`;
const lineNumbersWidth = layoutInfo.contentLeft;
lineNumberHTMLNode.style.width = `${lineNumbersWidth}px`;
const innerLineNumberHTML = document.createElement('span');
if (lineNumberOption.renderType === RenderLineNumbersType.On || lineNumberOption.renderType === RenderLineNumbersType.Interval && line % 10 === 0) {
innerLineNumberHTML.innerText = line.toString();
} else if (lineNumberOption.renderType === RenderLineNumbersType.Relative) {
innerLineNumberHTML.innerText = Math.abs(line - this._editor.getPosition()!.lineNumber).toString();
}
innerLineNumberHTML.className = 'sticky-line-number-inner';
innerLineNumberHTML.style.width = `${layoutInfo.lineNumbersWidth}px`;
innerLineNumberHTML.style.paddingLeft = `${layoutInfo.lineNumbersLeft}px`;
lineNumberHTMLNode.appendChild(innerLineNumberHTML);
const foldingIcon = this._renderFoldingIconForLine(foldingModel, line);
if (foldingIcon) {
lineNumberHTMLNode.appendChild(foldingIcon.domNode);
foldingIcon.domNode.style.left = `${layoutInfo.lineNumbersWidth + layoutInfo.lineNumbersLeft}px`;
foldingIcon.domNode.style.lineHeight = `${lineHeight}px`;
}
this._editor.applyFontInfo(lineHTMLNode);
this._editor.applyFontInfo(lineNumberHTMLNode);
lineNumberHTMLNode.style.lineHeight = `${lineHeight}px`;
lineHTMLNode.style.lineHeight = `${lineHeight}px`;
lineNumberHTMLNode.style.height = `${lineHeight}px`;
lineHTMLNode.style.height = `${lineHeight}px`;
const renderedLine = new RenderedStickyLine(
this._editor,
viewModel,
layoutInfo,
foldingModel,
this._isOnGlyphMargin,
index,
line,
lineHTMLNode,
lineNumberHTMLNode,
foldingIcon,
renderOutput.characterMapping,
lineHTMLNode.scrollWidth,
lineHeight
line
);
return this._updatePosition(renderedLine, top, isLastLine);
}
@ -406,25 +324,6 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
return stickyLine;
}
private _renderFoldingIconForLine(foldingModel: FoldingModel | undefined, line: number): StickyFoldingIcon | undefined {
const showFoldingControls: 'mouseover' | 'always' | 'never' = this._editor.getOption(EditorOption.showFoldingControls);
if (!foldingModel || showFoldingControls === 'never') {
return;
}
const foldingRegions = foldingModel.regions;
const indexOfFoldingRegion = foldingRegions.findRange(line);
const startLineNumber = foldingRegions.getStartLineNumber(indexOfFoldingRegion);
const isFoldingScope = line === startLineNumber;
if (!isFoldingScope) {
return;
}
const isCollapsed = foldingRegions.isCollapsed(indexOfFoldingRegion);
const foldingIcon = new StickyFoldingIcon(isCollapsed, startLineNumber, foldingRegions.getEndLineNumber(indexOfFoldingRegion), this._lineHeight);
foldingIcon.setVisible(this._isOnGlyphMargin ? true : (isCollapsed || showFoldingControls === 'always'));
foldingIcon.domNode.setAttribute(STICKY_IS_FOLDING_ICON_ATTR, '');
return foldingIcon;
}
getId(): string {
return 'editor.contrib.stickyScrollWidget';
}
@ -522,16 +421,127 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
}
class RenderedStickyLine {
public readonly lineDomNode: HTMLElement;
public readonly lineNumberDomNode: HTMLElement;
public readonly foldingIcon: StickyFoldingIcon | undefined;
public readonly characterMapping: CharacterMapping;
public readonly scrollWidth: number;
public readonly height: number;
constructor(
editor: ICodeEditor,
viewModel: IViewModel,
layoutInfo: EditorLayoutInfo,
foldingModel: FoldingModel | undefined,
isOnGlyphMargin: boolean,
public readonly index: number,
public readonly lineNumber: number,
public readonly lineDomNode: HTMLElement,
public readonly lineNumberDomNode: HTMLElement,
public readonly foldingIcon: StickyFoldingIcon | undefined,
public readonly characterMapping: CharacterMapping,
public readonly scrollWidth: number,
public readonly height: number
) { }
) {
const viewLineNumber = viewModel.coordinatesConverter.convertModelPositionToViewPosition(new Position(lineNumber, 1)).lineNumber;
const lineRenderingData = viewModel.getViewLineRenderingData(viewLineNumber);
const lineNumberOption = editor.getOption(EditorOption.lineNumbers);
const verticalScrollbarSize = editor.getOption(EditorOption.scrollbar).verticalScrollbarSize;
let actualInlineDecorations: LineDecoration[];
try {
actualInlineDecorations = LineDecoration.filter(lineRenderingData.inlineDecorations, viewLineNumber, lineRenderingData.minColumn, lineRenderingData.maxColumn);
} catch (err) {
actualInlineDecorations = [];
}
const lineHeight = editor.getLineHeightForPosition(new Position(lineNumber, 1));
const textDirection = viewModel.getTextDirection(lineNumber);
const renderLineInput: RenderLineInput = new RenderLineInput(true, true, lineRenderingData.content,
lineRenderingData.continuesWithWrappedLine,
lineRenderingData.isBasicASCII, lineRenderingData.containsRTL, 0,
lineRenderingData.tokens, actualInlineDecorations,
lineRenderingData.tabSize, lineRenderingData.startVisibleColumn,
1, 1, 1, 500, 'none', true, true, null,
textDirection, verticalScrollbarSize
);
const sb = new StringBuilder(2000);
const renderOutput = renderViewLine(renderLineInput, sb);
this.characterMapping = renderOutput.characterMapping;
let newLine;
if (_ttPolicy) {
newLine = _ttPolicy.createHTML(sb.build());
} else {
newLine = sb.build();
}
const lineHTMLNode = document.createElement('span');
lineHTMLNode.setAttribute(STICKY_INDEX_ATTR, String(index));
lineHTMLNode.setAttribute(STICKY_IS_LINE_ATTR, '');
lineHTMLNode.setAttribute('role', 'listitem');
lineHTMLNode.tabIndex = 0;
lineHTMLNode.className = 'sticky-line-content';
lineHTMLNode.classList.add(`stickyLine${lineNumber}`);
lineHTMLNode.style.lineHeight = `${lineHeight}px`;
lineHTMLNode.innerHTML = newLine as string;
const lineNumberHTMLNode = document.createElement('span');
lineNumberHTMLNode.setAttribute(STICKY_INDEX_ATTR, String(index));
lineNumberHTMLNode.setAttribute(STICKY_IS_LINE_NUMBER_ATTR, '');
lineNumberHTMLNode.className = 'sticky-line-number';
lineNumberHTMLNode.style.lineHeight = `${lineHeight}px`;
const lineNumbersWidth = layoutInfo.contentLeft;
lineNumberHTMLNode.style.width = `${lineNumbersWidth}px`;
const innerLineNumberHTML = document.createElement('span');
if (lineNumberOption.renderType === RenderLineNumbersType.On || lineNumberOption.renderType === RenderLineNumbersType.Interval && lineNumber % 10 === 0) {
innerLineNumberHTML.innerText = lineNumber.toString();
} else if (lineNumberOption.renderType === RenderLineNumbersType.Relative) {
innerLineNumberHTML.innerText = Math.abs(lineNumber - editor.getPosition()!.lineNumber).toString();
}
innerLineNumberHTML.className = 'sticky-line-number-inner';
innerLineNumberHTML.style.width = `${layoutInfo.lineNumbersWidth}px`;
innerLineNumberHTML.style.paddingLeft = `${layoutInfo.lineNumbersLeft}px`;
lineNumberHTMLNode.appendChild(innerLineNumberHTML);
const foldingIcon = this._renderFoldingIconForLine(editor, foldingModel, lineNumber, lineHeight, isOnGlyphMargin);
if (foldingIcon) {
lineNumberHTMLNode.appendChild(foldingIcon.domNode);
foldingIcon.domNode.style.left = `${layoutInfo.lineNumbersWidth + layoutInfo.lineNumbersLeft}px`;
foldingIcon.domNode.style.lineHeight = `${lineHeight}px`;
}
editor.applyFontInfo(lineHTMLNode);
editor.applyFontInfo(lineNumberHTMLNode);
lineNumberHTMLNode.style.lineHeight = `${lineHeight}px`;
lineHTMLNode.style.lineHeight = `${lineHeight}px`;
lineNumberHTMLNode.style.height = `${lineHeight}px`;
lineHTMLNode.style.height = `${lineHeight}px`;
this.scrollWidth = lineHTMLNode.scrollWidth;
this.lineDomNode = lineHTMLNode;
this.lineNumberDomNode = lineNumberHTMLNode;
this.height = lineHeight;
}
private _renderFoldingIconForLine(editor: ICodeEditor, foldingModel: FoldingModel | undefined, line: number, lineHeight: number, isOnGlyphMargin: boolean): StickyFoldingIcon | undefined {
const showFoldingControls: 'mouseover' | 'always' | 'never' = editor.getOption(EditorOption.showFoldingControls);
if (!foldingModel || showFoldingControls === 'never') {
return;
}
const foldingRegions = foldingModel.regions;
const indexOfFoldingRegion = foldingRegions.findRange(line);
const startLineNumber = foldingRegions.getStartLineNumber(indexOfFoldingRegion);
const isFoldingScope = line === startLineNumber;
if (!isFoldingScope) {
return;
}
const isCollapsed = foldingRegions.isCollapsed(indexOfFoldingRegion);
const foldingIcon = new StickyFoldingIcon(isCollapsed, startLineNumber, foldingRegions.getEndLineNumber(indexOfFoldingRegion), lineHeight);
foldingIcon.setVisible(isOnGlyphMargin ? true : (isCollapsed || showFoldingControls === 'always'));
foldingIcon.domNode.setAttribute(STICKY_IS_FOLDING_ICON_ATTR, '');
return foldingIcon;
}
}
class StickyFoldingIcon {

View File

@ -708,6 +708,74 @@ suite('Editor Model - TextModel', () => {
]);
});
test('issue #65668: YAML file indented with 2 spaces', () => {
// Full YAML file from the issue - should detect as 2 spaces
assertGuess(true, 2, [
'version: 2',
'',
'jobs:',
' build:',
' docker:',
' - circleci/golang:1.11',
'',
' environment:',
' TEST_RESULTS: /tmp/test-results',
'',
' steps:',
' - checkout',
' - run: mkdir -p $TEST_RESULTS',
'',
' - restore_cache:',
' keys:',
' - v1-pkg-cache',
'',
' - run:',
' name: dep ensure',
' command: dep ensure -v',
'',
' - run:',
' name: Run unit tests',
' command: |',
' trap "go-junit-report <${TEST_RESULTS}/go-test.out > ${TEST_RESULTS}/go-test-report.xml" EXIT',
' go test -v ./... | tee ${TEST_RESULTS}/go-test.out',
'',
' - run:',
' name: Build',
' command: go build -v',
'',
' - save_cache:',
' key: v1-pkg-cache',
' paths:',
' - "/go/pkg"',
'',
' - store_artifacts:',
' path: /tmp/test-results',
' destination: raw-test-output',
'',
' - store_test_results:',
' path: /tmp/test-results',
]);
});
test('issue #249040: 4-space indent should win over 2-space when predominant', () => {
// File with mostly 4-space indents but some 2-space indents should detect as 4 spaces
assertGuess(true, 4, [
'function foo() {',
' let a = 1;',
' let b = 2;',
' if (true) {',
' console.log(a);',
' console.log(b);',
' }',
' const obj = {',
' x: 1,', // 2-space indent here
' y: 2', // 2-space indent here
' };',
' return obj;',
'}',
]);
});
test('validatePosition', () => {
const m = createTextModel('line one\nline two');

View File

@ -40,3 +40,11 @@ export const luaIndentationRules = {
decreaseIndentPattern: /^\s*((\b(elseif|else|end|until)\b)|(\})|(\)))/,
increaseIndentPattern: /^((?!(\-\-)).)*((\b(else|function|then|do|repeat)\b((?!\b(end|until)\b).)*)|(\{\s*))$/,
};
export const vbIndentationRules = {
// Decrease indent when line starts with End <keyword>, Else, ElseIf, Case, Catch, Finally, Loop, Next, Wend, Until
decreaseIndentPattern: /^\s*((End\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Else|ElseIf|Case|Catch|Finally|Loop|Next|Wend|Until)\b/i,
// Increase indent after lines ending with Then, or lines starting with If/While/For/Do/Select/Sub/Function/Class/etc (block-starting keywords)
// The pattern matches lines that start block structures but excludes lines that also end them (like single-line If...Then...End If)
increaseIndentPattern: /^\s*((If|ElseIf).*Then(?!\s+(End\s+If))\s*(('|REM).*)?$)|\b(Else|While|For|Do|Select\s+Case|Case|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Try|Catch|Finally|SyncLock|Using|Property|Get|Set|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\b(?!.*\bEnd\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\b).*(('|REM).*)?$/i,
};

3
src/vs/monaco.d.ts vendored
View File

@ -771,6 +771,9 @@ declare namespace monaco {
* Moves the range by the given amount of lines.
*/
delta(lineCount: number): Range;
/**
* Test if this range starts and ends on the same line.
*/
isSingleLine(): boolean;
static fromPositions(start: IPosition, end?: IPosition): Range;
/**