{"action":"created","comment":{"url":"https://api.github.com/repos/Microsoft/python-language-server/pulls/comments/219969348","pull_request_review_id":158270903,"id":219969348,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIxOTk2OTM0OA==","diff_hunk":"@@ -0,0 +1,643 @@\n+﻿// Python Tools for Visual Studio\n+// Copyright(c) Microsoft Corporation\n+// All rights reserved.\n+//\n+// Licensed under the Apache License, Version 2.0 (the License); you may not use\n+// this file except in compliance with the License. You may obtain a copy of the\n+// License at http://www.apache.org/licenses/LICENSE-2.0\n+//\n+// THIS CODE IS PROVIDED ON AN  *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS\n+// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY\n+// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,\n+// MERCHANTABLITY OR NON-INFRINGEMENT.\n+//\n+// See the Apache Version 2.0 License for specific language governing\n+// permissions and limitations under the License.\n+\n+using System;\n+using System.Collections.Generic;\n+using System.IO;\n+using System.Linq;\n+using Microsoft.PythonTools;\n+using Microsoft.PythonTools.Analysis;\n+using Microsoft.PythonTools.Analysis.Infrastructure;\n+using Microsoft.PythonTools.Parsing;\n+\n+namespace Microsoft.Python.LanguageServer.Implementation {\n+    /// <summary>\n+    /// LineFormatter formats lines of code to generally conform with PEP8.\n+    /// </summary>\n+    public class LineFormatter {\n+        private static readonly TextEdit[] NoEdits = Array.Empty<TextEdit>();\n+\n+        private readonly TokenizerWrapper _tokenizer;\n+        private readonly Dictionary<int, List<TokenExt>> _lineTokens;\n+\n+        /// <summary>\n+        /// Creates a LineFormatter from a reader. It will only read as much\n+        /// of the input as is needed to format the requested line.\n+        /// </summary>\n+        /// <param name=\"reader\">The code to be formatted. LineFormatter does not dispose of the reader.</param>\n+        /// <param name=\"languageVersion\">Language version to use in the tokenization format.</param>\n+        public LineFormatter(TextReader reader, PythonLanguageVersion languageVersion) {\n+            var tokenizer = new Tokenizer(languageVersion, options: TokenizerOptions.Verbatim | TokenizerOptions.VerbatimCommentsAndLineJoins | TokenizerOptions.GroupingRecovery);\n+            tokenizer.Initialize(reader);\n+            _tokenizer = new TokenizerWrapper(tokenizer);\n+            _lineTokens = new Dictionary<int, List<TokenExt>>();\n+        }\n+\n+        private void AddToken(TokenExt token) {\n+            var line = token.Line;\n+\n+            // Explicit line joins (\"\\\") appear at the end of a line, but\n+            // their span ends on another line, so move backward so they can\n+            // be inserted in the right place.\n+            if (token.Kind == TokenKind.ExplicitLineJoin) {\n+                line--;\n+            }\n+\n+            if (!_lineTokens.TryGetValue(line, out List<TokenExt> tokens)) {\n+                tokens = new List<TokenExt>();\n+                _lineTokens.Add(line, tokens);\n+            }\n+\n+            tokens.Add(token);\n+        }\n+\n+        /// <summary>\n+        /// Tokenizes up to and including the specified line. Tokens are\n+        /// stored in _lineTokens. If the provided line number is past the\n+        /// end of the input text, then the tokenizer will stop.\n+        /// Additionally, this function will attempt to read ahead onto the\n+        /// next line to the first non-ignored token so that the formatter\n+        /// can look ahead.\n+        /// </summary>\n+        /// <param name=\"line\">One-indexed line number.</param>\n+        /// <returns>A non-null list of tokens on that line.</returns>\n+        private List<TokenExt> TokenizeLine(int line) {\n+            if (line < 1) {\n+                throw new ArgumentOutOfRangeException(nameof(line));\n+            }\n+\n+            var extraToken = true;\n+\n+            var peeked = _tokenizer.Peek();\n+            while (peeked != null && (peeked.Line <= line || extraToken)) {\n+                var token = _tokenizer.Next();\n+                AddToken(token);\n+                peeked = _tokenizer.Peek();\n+\n+                if (token.Line > line && !token.IsIgnored) {\n+                    extraToken = false;\n+                }\n+            }\n+\n+            if (!_lineTokens.TryGetValue(line, out List<TokenExt> tokens)) {\n+                return new List<TokenExt>();\n+            }\n+\n+            return tokens;\n+        }\n+\n+        /// <summary>\n+        /// Formats a single line and returns TextEdits to replace the old text.\n+        /// </summary>\n+        /// <param name=\"line\">One-indexed line number.</param>\n+        /// <returns>A list of TextEdits needed to format the line.</returns>\n+        public TextEdit[] FormatLine(int line) {\n+            if (line < 1) {\n+                return NoEdits;\n+            }\n+\n+            var tokens = TokenizeLine(line);\n+\n+            // Keep ExplictLineJoin because it has text associated with it.\n+            tokens = tokens.Where(t => !t.IsIgnored || t.Kind == TokenKind.ExplicitLineJoin).ToList();\n+\n+            if (tokens.Count == 0) {\n+                return NoEdits;\n+            }\n+\n+            var builder = new TextBuilder();\n+            var first = tokens[0];\n+            var beginCol = first.Span.Start.Column;\n+            var startIdx = 0;\n+\n+            if (first.IsMultilineString) {\n+                // If the first token is a multiline string, start the edit afterward,\n+                // skip looking at the first token, and ensure that there's a space\n+                // after it if needed (i.e. in the case of a following comment).\n+                beginCol = first.Span.End.Column;\n+                startIdx = 1;\n+                builder.SoftAppendSpace(allowLeading: true);\n+            }\n+\n+            for (var i = startIdx; i < tokens.Count; i++) {\n+                var token = tokens[i];\n+                var prev = tokens.ElementAtOrDefault(i - 1);\n+                var next = tokens.ElementAtOrDefault(i + 1);\n+\n+                switch (token.Kind) {\n+                    case TokenKind.Comment:\n+                        builder.SoftAppendSpace(2);\n+                        builder.Append(token);\n+                        break;\n+\n+                    case TokenKind.Assign:\n+                        if (token.IsInsideFunctionArgs && prev?.PrevNonIgnored?.Kind != TokenKind.Colon) {\n+                            builder.Append(token);\n+                            break;\n+                        }\n+\n+                        goto case TokenKind.AddEqual;\n+\n+                    // \"Normal\" assignment and function parameters with type hints\n+                    case TokenKind.AddEqual:\n+                    case TokenKind.SubtractEqual:\n+                    case TokenKind.PowerEqual:\n+                    case TokenKind.MultiplyEqual:\n+                    case TokenKind.MatMultiplyEqual:\n+                    case TokenKind.FloorDivideEqual:\n+                    case TokenKind.DivideEqual:\n+                    case TokenKind.ModEqual:\n+                    case TokenKind.LeftShiftEqual:\n+                    case TokenKind.RightShiftEqual:\n+                    case TokenKind.BitwiseAndEqual:\n+                    case TokenKind.BitwiseOrEqual:\n+                    case TokenKind.ExclusiveOrEqual:\n+                        builder.SoftAppendSpace();\n+                        builder.Append(token);\n+                        builder.SoftAppendSpace();\n+                        break;\n+\n+                    case TokenKind.Comma:\n+                        builder.Append(token);\n+                        if (next != null && !next.IsClose && next.Kind != TokenKind.Colon) {\n+                            builder.SoftAppendSpace();\n+                        }\n+                        break;\n+\n+                    case TokenKind.Colon:\n+                        // Slicing\n+                        if (token.Inside?.Kind == TokenKind.LeftBracket) {\n+                            if (!token.IsSimpleSliceToLeft) {\n+                                builder.SoftAppendSpace();\n+                            }\n+\n+                            builder.Append(token);\n+\n+                            if (!token.IsSimpleSliceToRight) {\n+                                builder.SoftAppendSpace();\n+                            }\n+\n+                            break;\n+                        }\n+\n+                        builder.Append(token);\n+                        if (next != null && !next.Is(TokenKind.Colon, TokenKind.Comma)) {\n+                            builder.SoftAppendSpace();\n+                        }\n+                        break;\n+\n+                    case TokenKind.At:\n+                        if (prev != null) {\n+                            goto case TokenKind.MatMultiply;\n+                        }\n+\n+                        builder.Append(token);\n+                        break;\n+\n+                    // Unary\n+                    case TokenKind.Add:\n+                    case TokenKind.Subtract:\n+                    case TokenKind.Twiddle:\n+                        if (prev != null && (prev.IsOperator || prev.IsOpen || prev.Is(TokenKind.Comma, TokenKind.Colon))) {\n+                            builder.Append(token);\n+                            break;\n+                        }\n+                        goto case TokenKind.MatMultiply;\n+\n+                    case TokenKind.Power:\n+                    case TokenKind.Multiply:\n+                        if (token.Inside != null) {\n+                            var actualPrev = token.PrevNonIgnored;\n+                            if (actualPrev != null) {\n+                                if (actualPrev.Kind == TokenKind.Comma || actualPrev.IsOpen || token.Inside.Kind == TokenKind.KeywordLambda) {\n+                                    builder.Append(token);\n+                                    break;\n+                                }\n+                            }\n+                        }\n+\n+                        if (token.Kind == TokenKind.Multiply) {\n+                            // Check unpacking case\n+                            var actualPrev = token.PrevNonIgnored;\n+                            if (actualPrev == null || (actualPrev.Kind != TokenKind.Name && actualPrev.Kind != TokenKind.Constant && !actualPrev.IsClose)) {\n+                                builder.Append(token);\n+                                break;\n+                            }\n+                        }\n+\n+                        goto case TokenKind.MatMultiply;\n+\n+                    // Operators\n+                    case TokenKind.MatMultiply:\n+                    case TokenKind.FloorDivide:\n+                    case TokenKind.Divide:\n+                    case TokenKind.Mod:\n+                    case TokenKind.LeftShift:\n+                    case TokenKind.RightShift:\n+                    case TokenKind.BitwiseAnd:\n+                    case TokenKind.BitwiseOr:\n+                    case TokenKind.ExclusiveOr:\n+                    case TokenKind.LessThan:\n+                    case TokenKind.GreaterThan:\n+                    case TokenKind.LessThanOrEqual:\n+                    case TokenKind.GreaterThanOrEqual:\n+                    case TokenKind.Equals:\n+                    case TokenKind.NotEquals:\n+                    case TokenKind.LessThanGreaterThan:\n+                    case TokenKind.Arrow:\n+                        builder.SoftAppendSpace();\n+                        builder.Append(token);\n+                        builder.SoftAppendSpace();\n+                        break;\n+\n+                    case TokenKind.Dot:\n+                        if (prev != null && (prev.Kind == TokenKind.KeywordFrom || prev.IsNumber)) {\n+                            builder.SoftAppendSpace();\n+                        }\n+\n+                        builder.Append(token);\n+                        break;\n+\n+                    case TokenKind.LeftBrace:\n+                    case TokenKind.LeftBracket:\n+                    case TokenKind.LeftParenthesis:\n+                    case TokenKind.RightBrace:\n+                    case TokenKind.RightBracket:\n+                    case TokenKind.RightParenthesis:\n+                        builder.Append(token);\n+                        break;\n+\n+                    case TokenKind.Semicolon:\n+                        builder.Append(token);\n+                        builder.SoftAppendSpace();\n+                        break;\n+\n+                    case TokenKind.Name:\n+                    case TokenKind.Constant:\n+                    case TokenKind.KeywordFalse:\n+                    case TokenKind.KeywordTrue:\n+                    case TokenKind.Ellipsis: // Ellipsis is a value\n+                        builder.Append(token);\n+                        break;\n+\n+                    case TokenKind.ExplicitLineJoin:\n+                        builder.SoftAppendSpace();\n+                        builder.Append(\"\\\\\"); // Hardcoded string so that any following whitespace doesn't make it in.\n+                        break;\n+\n+                    case TokenKind.BackQuote:\n+                        builder.Append(token);\n+                        break;\n+\n+                    default:\n+                        if (token.Kind == TokenKind.KeywordLambda) {\n+                            if (token.IsInsideFunctionArgs && prev?.Kind == TokenKind.Assign) {\n+                                builder.Append(token);\n+\n+                                if (next?.Kind != TokenKind.Colon) {\n+                                    builder.SoftAppendSpace();\n+                                }\n+\n+                                break;\n+                            }\n+                        }\n+\n+                        if (token.IsKeyword) {\n+                            if (prev != null && !prev.IsOpen) {\n+                                builder.SoftAppendSpace();\n+                            }\n+\n+                            builder.Append(token);\n+\n+                            if (next != null && next.Kind != TokenKind.Colon && next.Kind != TokenKind.Semicolon) {\n+                                builder.SoftAppendSpace();\n+                            }\n+\n+                            break;\n+                        }\n+\n+                        // No tokens should make it to this case, but try to keep things separated.\n+                        builder.SoftAppendSpace();\n+                        builder.Append(token);\n+                        builder.SoftAppendSpace();\n+                        break;\n+                }\n+            }\n+\n+            var endCol = _tokenizer.EndOfLineCol(line);\n+\n+            var afterLast = tokens.Last().Next;\n+            if (afterLast != null && afterLast.IsMultilineString) {\n+                // If the the next token is a multiline string, then make\n+                // sure to include that string's prefix on this line.\n+                var afterLastFirst = SplitByNewline(afterLast.ToString()).First();\n+                builder.Append(afterLastFirst);\n+            }\n+\n+            var newText = builder.ToString();\n+\n+            if (newText.Length == 0) {\n+                return NoEdits;\n+            }\n+\n+            var edit = new TextEdit {\n+                range = new Range {\n+                    start = new SourceLocation(line, beginCol),\n+                    end = new SourceLocation(line, endCol)\n+                },\n+                newText = newText\n+            };\n+\n+            return new[] { edit };\n+        }\n+\n+        private class TokenExt {\n+            public Token Token { get; set; }\n+            public SourceSpan Span { get; set; }\n+            public int Line => Span.End.Line;\n+            public TokenExt Inside { get; set; }\n+            public TokenExt Prev { get; set; }\n+            public TokenExt Next { get; set; }\n+            public string PreceedingWhitespace { get; set; }\n+            public TokenKind Kind => Token.Kind;\n+\n+            public override string ToString() => Token.VerbatimImage;\n+\n+            public bool Is(params TokenKind[] kinds) => kinds.Contains(Kind);\n+\n+            public bool IsIgnored => Is(TokenKind.NewLine, TokenKind.NLToken, TokenKind.Indent, TokenKind.Dedent, TokenKind.ExplicitLineJoin);\n+\n+            public bool IsOpen => Is(TokenKind.LeftBrace, TokenKind.LeftBracket, TokenKind.LeftParenthesis);\n+\n+            public bool IsClose => Is(TokenKind.RightBrace, TokenKind.RightBracket, TokenKind.RightParenthesis);\n+\n+            public bool MatchesClose(TokenExt other) {\n+                switch (Kind) {\n+                    case TokenKind.LeftBrace:\n+                        return other.Kind == TokenKind.RightBrace;\n+                    case TokenKind.LeftBracket:\n+                        return other.Kind == TokenKind.RightBracket;\n+                    case TokenKind.LeftParenthesis:\n+                        return other.Kind == TokenKind.RightParenthesis;\n+                }\n+\n+                return false;\n+            }\n+\n+            public bool IsOperator => Token is OperatorToken || Is(TokenKind.Dot, TokenKind.Assign, TokenKind.Twiddle);\n+\n+            public bool IsUnaryOp => Is(TokenKind.Add, TokenKind.Subtract, TokenKind.Twiddle);\n+\n+            public bool IsInsideFunctionArgs => (Inside?.Kind == TokenKind.LeftParenthesis && Inside.PrevNonIgnored?.Kind == TokenKind.Name) || (Inside?.Kind == TokenKind.KeywordLambda);\n+\n+            public bool IsNumber => Kind == TokenKind.Constant && Token != Tokens.NoneToken && !(Token.Value is string || Token.Value is AsciiString);\n+\n+            public bool IsKeyword => (Kind >= TokenKind.FirstKeyword && Kind <= TokenKind.LastKeyword) || Kind == TokenKind.KeywordAsync || Kind == TokenKind.KeywordAwait;\n+\n+            public bool IsMultilineString\n+            {\n+                get\n+                {\n+                    if (Span.Start.Line == Span.End.Line) {\n+                        return false;\n+                    }\n+\n+                    if (Kind != TokenKind.Constant || Token == Tokens.NoneToken) {\n+                        return false;\n+                    }\n+\n+                    return Token.Value is string || Token.Value is AsciiString;\n+                }\n+            }\n+\n+            public bool IsSimpleSliceToLeft\n+            {\n+                get\n+                {\n+                    if (Kind != TokenKind.Colon) {\n+                        return false;\n+                    }\n+\n+                    var a = PrevNonIgnored;\n+                    var b = a?.PrevNonIgnored;\n+                    var c = b?.PrevNonIgnored;\n+\n+                    if (a == null) {\n+                        return false;\n+                    }\n+\n+                    if (a.Is(TokenKind.LeftBracket, TokenKind.Colon)) {\n+                        return true;\n+                    }\n+\n+                    if ((!a.IsNumber && a.Kind != TokenKind.Name) || b == null) {\n+                        return false;\n+                    }\n+\n+                    if (b.Is(TokenKind.LeftBracket, TokenKind.Colon, TokenKind.Comma)) {\n+                        return true;\n+                    }\n+\n+                    if (!b.IsUnaryOp || c == null) {\n+                        return false;\n+                    }\n+\n+                    return c.Is(TokenKind.LeftBracket, TokenKind.Colon, TokenKind.Comma);\n+                }\n+            }\n+\n+            public bool IsSimpleSliceToRight\n+            {\n+                get\n+                {\n+                    if (Kind != TokenKind.Colon) {\n+                        return false;\n+                    }\n+\n+                    var a = NextNonIgnored;\n+                    var b = a?.NextNonIgnored;\n+                    var c = b?.NextNonIgnored;\n+\n+                    if (a == null) {\n+                        return false;\n+                    }\n+\n+                    if (a.Is(TokenKind.RightBracket, TokenKind.Colon, TokenKind.Comma)) {\n+                        return true;\n+                    }\n+\n+                    if (b == null) {\n+                        return false;\n+                    }\n+\n+                    if (a.IsUnaryOp) {\n+                        if (c == null) {\n+                            return false;\n+                        }\n+                        return (b.IsNumber || b.Kind == TokenKind.Name) && c.Is(TokenKind.RightBracket, TokenKind.Colon, TokenKind.Comma);\n+                    }\n+\n+                    return (a.IsNumber || a.Kind == TokenKind.Name) && b.Is(TokenKind.RightBracket, TokenKind.Colon, TokenKind.Comma);\n+                }\n+            }\n+\n+            public TokenExt PrevNonIgnored\n+            {\n+                get\n+                {","path":"src/LanguageServer/Impl/Implementation/LineFormatter.cs","position":500,"original_position":500,"commit_id":"e1829041949f5225937e5897a73a8d133cabc9a9","original_commit_id":"e1829041949f5225937e5897a73a8d133cabc9a9","user":{"login":"MikhailArkhipov","id":12820357,"node_id":"MDQ6VXNlcjEyODIwMzU3","avatar_url":"https://avatars2.githubusercontent.com/u/12820357?v=4","gravatar_id":"","url":"https://api.github.com/users/MikhailArkhipov","html_url":"https://github.com/MikhailArkhipov","followers_url":"https://api.github.com/users/MikhailArkhipov/followers","following_url":"https://api.github.com/users/MikhailArkhipov/following{/other_user}","gists_url":"https://api.github.com/users/MikhailArkhipov/gists{/gist_id}","starred_url":"https://api.github.com/users/MikhailArkhipov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MikhailArkhipov/subscriptions","organizations_url":"https://api.github.com/users/MikhailArkhipov/orgs","repos_url":"https://api.github.com/users/MikhailArkhipov/repos","events_url":"https://api.github.com/users/MikhailArkhipov/events{/privacy}","received_events_url":"https://api.github.com/users/MikhailArkhipov/received_events","type":"User","site_admin":false},"body":"Formatting","created_at":"2018-09-24T20:00:50Z","updated_at":"2018-09-24T20:00:50Z","html_url":"https://github.com/Microsoft/python-language-server/pull/126#discussion_r219969348","pull_request_url":"https://api.github.com/repos/Microsoft/python-language-server/pulls/126","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/Microsoft/python-language-server/pulls/comments/219969348"},"html":{"href":"https://github.com/Microsoft/python-language-server/pull/126#discussion_r219969348"},"pull_request":{"href":"https://api.github.com/repos/Microsoft/python-language-server/pulls/126"}}},"pull_request":{"url":"https://api.github.com/repos/Microsoft/python-language-server/pulls/126","id":217760468,"node_id":"MDExOlB1bGxSZXF1ZXN0MjE3NzYwNDY4","html_url":"https://github.com/Microsoft/python-language-server/pull/126","diff_url":"https://github.com/Microsoft/python-language-server/pull/126.diff","patch_url":"https://github.com/Microsoft/python-language-server/pull/126.patch","issue_url":"https://api.github.com/repos/Microsoft/python-language-server/issues/126","number":126,"state":"open","locked":false,"title":"On type formatting","user":{"login":"jakebailey","id":5341706,"node_id":"MDQ6VXNlcjUzNDE3MDY=","avatar_url":"https://avatars2.githubusercontent.com/u/5341706?v=4","gravatar_id":"","url":"https://api.github.com/users/jakebailey","html_url":"https://github.com/jakebailey","followers_url":"https://api.github.com/users/jakebailey/followers","following_url":"https://api.github.com/users/jakebailey/following{/other_user}","gists_url":"https://api.github.com/users/jakebailey/gists{/gist_id}","starred_url":"https://api.github.com/users/jakebailey/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jakebailey/subscriptions","organizations_url":"https://api.github.com/users/jakebailey/orgs","repos_url":"https://api.github.com/users/jakebailey/repos","events_url":"https://api.github.com/users/jakebailey/events{/privacy}","received_events_url":"https://api.github.com/users/jakebailey/received_events","type":"User","site_admin":false},"body":"This PR implements line formatting via on-type formatting, triggering on newline and semicolon. It only tokenizes the code to be formatted, and does not require parsing.\r\n\r\nThis covers all of the existing formatOnType issues in vscode-python (Microsoft/vscode-python#1799, Microsoft/vscode-python#1784, Microsoft/vscode-python#1792), as well as some other cases that weren't being handled already (some multiline stuff, commas in brackets, all of the different types of unpacking, more).\r\n\r\nSlicing follows the PEP8 pet peeves example list by using a simple heuristic to decide when to insert a space. If the thing to the left/right of a colon is \"simple\" (a single identifier/constant, with or without a unary operator), then no space is inserted. The heuristic also checks for commas to handle multi-index slicing.\r\n\r\nTests have been copied from vscode-python, with some minor modifications for PEP8 correctness. Extra tests have been added for the new fixes. \r\n\r\nSome issues/possible improvements:\r\n\r\n- TextBuilder is in Analysis.Engine.Infrastructure, but the rest of the formatting code is in LanguageServer.Implementation. This is probably wrong.\r\n- The heuristic used for spacing out slicing is pretty simple, and not the same one used by tools like black. Using an existing algorithm may be preferred to prevent situations where you hit enter, get one thing, format the document, get another, hit enter again and go back, and so on. Perhaps it would be even better to outsource line formatting to external tools if they're enabled for (upcoming) full-document formatting.\r\n- The existing tokenizer does not capture the line continuation token if there's no newline after the `\\`. Since the token is never captured, the formatter won't know it's there and will not insert it. This issue isn't hit in the editor when hitting enter after the `\\`, since the formatter is called after the newline gets inserted. The only way to trigger this issue from the editor is to hit `;` when there is no next line, or to somehow invoke a DocumentOnTypeFormatter RPC call directly without inserting the newline.\r\n    - The easiest fix would be to just append a newline to the end of the incoming TextReader. Since the formatter only works with tokens, and by request, there's no way this extra newline would ever make it back into the output.\r\n    - Alternatively, the tokenizer could be modified to treat the `\\` token separately from the following newline, but I haven't investigated the implications of that change.\r\n- Unknown tokens will currently just have spaces put around them. It may be better to throw an execption or spit out a warning.\r\n- This formatter could be converted into a makeshift full-document formatter by formatting each line and returning the edits (or being smarter and combining them into a single edit). It might be an acceptable default when no other document formatter exists.","created_at":"2018-09-24T19:19:47Z","updated_at":"2018-09-24T20:00:50Z","closed_at":null,"merged_at":null,"merge_commit_sha":"bb789d960d67eca495a1e6b5d9778a0b9feb63d6","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/Microsoft/python-language-server/pulls/126/commits","review_comments_url":"https://api.github.com/repos/Microsoft/python-language-server/pulls/126/comments","review_comment_url":"https://api.github.com/repos/Microsoft/python-language-server/pulls/comments{/number}","comments_url":"https://api.github.com/repos/Microsoft/python-language-server/issues/126/comments","statuses_url":"https://api.github.com/repos/Microsoft/python-language-server/statuses/e1829041949f5225937e5897a73a8d133cabc9a9","head":{"label":"jakebailey:on-type-formatting","ref":"on-type-formatting","sha":"e1829041949f5225937e5897a73a8d133cabc9a9","user":{"login":"jakebailey","id":5341706,"node_id":"MDQ6VXNlcjUzNDE3MDY=","avatar_url":"https://avatars2.githubusercontent.com/u/5341706?v=4","gravatar_id":"","url":"https://api.github.com/users/jakebailey","html_url":"https://github.com/jakebailey","followers_url":"https://api.github.com/users/jakebailey/followers","following_url":"https://api.github.com/users/jakebailey/following{/other_user}","gists_url":"https://api.github.com/users/jakebailey/gists{/gist_id}","starred_url":"https://api.github.com/users/jakebailey/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jakebailey/subscriptions","organizations_url":"https://api.github.com/users/jakebailey/orgs","repos_url":"https://api.github.com/users/jakebailey/repos","events_url":"https://api.github.com/users/jakebailey/events{/privacy}","received_events_url":"https://api.github.com/users/jakebailey/received_events","type":"User","site_admin":false},"repo":{"id":147745004,"node_id":"MDEwOlJlcG9zaXRvcnkxNDc3NDUwMDQ=","name":"python-language-server","full_name":"jakebailey/python-language-server","private":false,"owner":{"login":"jakebailey","id":5341706,"node_id":"MDQ6VXNlcjUzNDE3MDY=","avatar_url":"https://avatars2.githubusercontent.com/u/5341706?v=4","gravatar_id":"","url":"https://api.github.com/users/jakebailey","html_url":"https://github.com/jakebailey","followers_url":"https://api.github.com/users/jakebailey/followers","following_url":"https://api.github.com/users/jakebailey/following{/other_user}","gists_url":"https://api.github.com/users/jakebailey/gists{/gist_id}","starred_url":"https://api.github.com/users/jakebailey/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jakebailey/subscriptions","organizations_url":"https://api.github.com/users/jakebailey/orgs","repos_url":"https://api.github.com/users/jakebailey/repos","events_url":"https://api.github.com/users/jakebailey/events{/privacy}","received_events_url":"https://api.github.com/users/jakebailey/received_events","type":"User","site_admin":false},"html_url":"https://github.com/jakebailey/python-language-server","description":"Microsoft Language Server for Python","fork":true,"url":"https://api.github.com/repos/jakebailey/python-language-server","forks_url":"https://api.github.com/repos/jakebailey/python-language-server/forks","keys_url":"https://api.github.com/repos/jakebailey/python-language-server/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jakebailey/python-language-server/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jakebailey/python-language-server/teams","hooks_url":"https://api.github.com/repos/jakebailey/python-language-server/hooks","issue_events_url":"https://api.github.com/repos/jakebailey/python-language-server/issues/events{/number}","events_url":"https://api.github.com/repos/jakebailey/python-language-server/events","assignees_url":"https://api.github.com/repos/jakebailey/python-language-server/assignees{/user}","branches_url":"https://api.github.com/repos/jakebailey/python-language-server/branches{/branch}","tags_url":"https://api.github.com/repos/jakebailey/python-language-server/tags","blobs_url":"https://api.github.com/repos/jakebailey/python-language-server/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jakebailey/python-language-server/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jakebailey/python-language-server/git/refs{/sha}","trees_url":"https://api.github.com/repos/jakebailey/python-language-server/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jakebailey/python-language-server/statuses/{sha}","languages_url":"https://api.github.com/repos/jakebailey/python-language-server/languages","stargazers_url":"https://api.github.com/repos/jakebailey/python-language-server/stargazers","contributors_url":"https://api.github.com/repos/jakebailey/python-language-server/contributors","subscribers_url":"https://api.github.com/repos/jakebailey/python-language-server/subscribers","subscription_url":"https://api.github.com/repos/jakebailey/python-language-server/subscription","commits_url":"https://api.github.com/repos/jakebailey/python-language-server/commits{/sha}","git_commits_url":"https://api.github.com/repos/jakebailey/python-language-server/git/commits{/sha}","comments_url":"https://api.github.com/repos/jakebailey/python-language-server/comments{/number}","issue_comment_url":"https://api.github.com/repos/jakebailey/python-language-server/issues/comments{/number}","contents_url":"https://api.github.com/repos/jakebailey/python-language-server/contents/{+path}","compare_url":"https://api.github.com/repos/jakebailey/python-language-server/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jakebailey/python-language-server/merges","archive_url":"https://api.github.com/repos/jakebailey/python-language-server/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jakebailey/python-language-server/downloads","issues_url":"https://api.github.com/repos/jakebailey/python-language-server/issues{/number}","pulls_url":"https://api.github.com/repos/jakebailey/python-language-server/pulls{/number}","milestones_url":"https://api.github.com/repos/jakebailey/python-language-server/milestones{/number}","notifications_url":"https://api.github.com/repos/jakebailey/python-language-server/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jakebailey/python-language-server/labels{/name}","releases_url":"https://api.github.com/repos/jakebailey/python-language-server/releases{/id}","deployments_url":"https://api.github.com/repos/jakebailey/python-language-server/deployments","created_at":"2018-09-06T23:31:25Z","updated_at":"2018-09-24T18:05:35Z","pushed_at":"2018-09-24T18:48:26Z","git_url":"git://github.com/jakebailey/python-language-server.git","ssh_url":"git@github.com:jakebailey/python-language-server.git","clone_url":"https://github.com/jakebailey/python-language-server.git","svn_url":"https://github.com/jakebailey/python-language-server","homepage":null,"size":1803,"stargazers_count":0,"watchers_count":0,"language":"C#","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"Microsoft:master","ref":"master","sha":"291b59dd4a38166998add2ea821c0da94f6cc746","user":{"login":"Microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/Microsoft","html_url":"https://github.com/Microsoft","followers_url":"https://api.github.com/users/Microsoft/followers","following_url":"https://api.github.com/users/Microsoft/following{/other_user}","gists_url":"https://api.github.com/users/Microsoft/gists{/gist_id}","starred_url":"https://api.github.com/users/Microsoft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Microsoft/subscriptions","organizations_url":"https://api.github.com/users/Microsoft/orgs","repos_url":"https://api.github.com/users/Microsoft/repos","events_url":"https://api.github.com/users/Microsoft/events{/privacy}","received_events_url":"https://api.github.com/users/Microsoft/received_events","type":"Organization","site_admin":false},"repo":{"id":145901977,"node_id":"MDEwOlJlcG9zaXRvcnkxNDU5MDE5Nzc=","name":"python-language-server","full_name":"Microsoft/python-language-server","private":false,"owner":{"login":"Microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/Microsoft","html_url":"https://github.com/Microsoft","followers_url":"https://api.github.com/users/Microsoft/followers","following_url":"https://api.github.com/users/Microsoft/following{/other_user}","gists_url":"https://api.github.com/users/Microsoft/gists{/gist_id}","starred_url":"https://api.github.com/users/Microsoft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Microsoft/subscriptions","organizations_url":"https://api.github.com/users/Microsoft/orgs","repos_url":"https://api.github.com/users/Microsoft/repos","events_url":"https://api.github.com/users/Microsoft/events{/privacy}","received_events_url":"https://api.github.com/users/Microsoft/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/Microsoft/python-language-server","description":"Microsoft Language Server for Python","fork":false,"url":"https://api.github.com/repos/Microsoft/python-language-server","forks_url":"https://api.github.com/repos/Microsoft/python-language-server/forks","keys_url":"https://api.github.com/repos/Microsoft/python-language-server/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Microsoft/python-language-server/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Microsoft/python-language-server/teams","hooks_url":"https://api.github.com/repos/Microsoft/python-language-server/hooks","issue_events_url":"https://api.github.com/repos/Microsoft/python-language-server/issues/events{/number}","events_url":"https://api.github.com/repos/Microsoft/python-language-server/events","assignees_url":"https://api.github.com/repos/Microsoft/python-language-server/assignees{/user}","branches_url":"https://api.github.com/repos/Microsoft/python-language-server/branches{/branch}","tags_url":"https://api.github.com/repos/Microsoft/python-language-server/tags","blobs_url":"https://api.github.com/repos/Microsoft/python-language-server/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Microsoft/python-language-server/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Microsoft/python-language-server/git/refs{/sha}","trees_url":"https://api.github.com/repos/Microsoft/python-language-server/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Microsoft/python-language-server/statuses/{sha}","languages_url":"https://api.github.com/repos/Microsoft/python-language-server/languages","stargazers_url":"https://api.github.com/repos/Microsoft/python-language-server/stargazers","contributors_url":"https://api.github.com/repos/Microsoft/python-language-server/contributors","subscribers_url":"https://api.github.com/repos/Microsoft/python-language-server/subscribers","subscription_url":"https://api.github.com/repos/Microsoft/python-language-server/subscription","commits_url":"https://api.github.com/repos/Microsoft/python-language-server/commits{/sha}","git_commits_url":"https://api.github.com/repos/Microsoft/python-language-server/git/commits{/sha}","comments_url":"https://api.github.com/repos/Microsoft/python-language-server/comments{/number}","issue_comment_url":"https://api.github.com/repos/Microsoft/python-language-server/issues/comments{/number}","contents_url":"https://api.github.com/repos/Microsoft/python-language-server/contents/{+path}","compare_url":"https://api.github.com/repos/Microsoft/python-language-server/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Microsoft/python-language-server/merges","archive_url":"https://api.github.com/repos/Microsoft/python-language-server/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Microsoft/python-language-server/downloads","issues_url":"https://api.github.com/repos/Microsoft/python-language-server/issues{/number}","pulls_url":"https://api.github.com/repos/Microsoft/python-language-server/pulls{/number}","milestones_url":"https://api.github.com/repos/Microsoft/python-language-server/milestones{/number}","notifications_url":"https://api.github.com/repos/Microsoft/python-language-server/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Microsoft/python-language-server/labels{/name}","releases_url":"https://api.github.com/repos/Microsoft/python-language-server/releases{/id}","deployments_url":"https://api.github.com/repos/Microsoft/python-language-server/deployments","created_at":"2018-08-23T20:09:46Z","updated_at":"2018-09-24T19:24:28Z","pushed_at":"2018-09-24T19:24:26Z","git_url":"git://github.com/Microsoft/python-language-server.git","ssh_url":"git@github.com:Microsoft/python-language-server.git","clone_url":"https://github.com/Microsoft/python-language-server.git","svn_url":"https://github.com/Microsoft/python-language-server","homepage":null,"size":1735,"stargazers_count":27,"watchers_count":27,"language":"C#","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":6,"mirror_url":null,"archived":false,"open_issues_count":45,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":6,"open_issues":45,"watchers":27,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/Microsoft/python-language-server/pulls/126"},"html":{"href":"https://github.com/Microsoft/python-language-server/pull/126"},"issue":{"href":"https://api.github.com/repos/Microsoft/python-language-server/issues/126"},"comments":{"href":"https://api.github.com/repos/Microsoft/python-language-server/issues/126/comments"},"review_comments":{"href":"https://api.github.com/repos/Microsoft/python-language-server/pulls/126/comments"},"review_comment":{"href":"https://api.github.com/repos/Microsoft/python-language-server/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/Microsoft/python-language-server/pulls/126/commits"},"statuses":{"href":"https://api.github.com/repos/Microsoft/python-language-server/statuses/e1829041949f5225937e5897a73a8d133cabc9a9"}},"author_association":"MEMBER"}}
{"actor":{"display_login":"MikhailArkhipov"}}