diff --git a/packages/language-csharp/grammars/tree-sitter-c-sharp/highlights.scm b/packages/language-csharp/grammars/tree-sitter-c-sharp/highlights.scm index eabeb7a97d..9957efae79 100644 --- a/packages/language-csharp/grammars/tree-sitter-c-sharp/highlights.scm +++ b/packages/language-csharp/grammars/tree-sitter-c-sharp/highlights.scm @@ -10,6 +10,8 @@ "params" "partial" "static" + "async" + "readonly" "unchecked" "unmanaged" ] @storage.modifier._TYPE_.cs @@ -63,10 +65,45 @@ (raw_string_literal) @string.quoted.triple.cs (verbatim_string_literal) @string.quoted.double.verbatim.cs +; Double-quoted interpolated string expressions. We want to match `$""` and +; `$"x"` but not `$"""`. Also, `$`/`$@`/`@$` are all valid sigils! +((interpolated_string_expression) @string.quoted.double.interpolated.cs + (#match? @string.quoted.double.interpolated.cs "^(\\$|\\$@|@\\$)\"[^\"]")) +((interpolated_string_expression) @string.quoted.double.interpolated.cs + (#eq? @string.quoted.double.interpolated.cs "$\"\"")) + +; Triple-quoted interpolation strings. +(interpolated_string_expression (interpolation_quote)) @string.quoted.triple.interpolated.cs + +; Delimiters for triple-quoted interpolation strings. +(interpolated_string_expression + (interpolation_quote) @punctuation.definition.string.begin.cs + (#is? test.firstOfType)) +(interpolated_string_expression + (interpolation_quote) @punctuation.definition.string.end.cs + (#is? test.lastOfType)) + +; The sigil in an interpolation string. +(interpolation_start) @punctuation.definition.string.begin.cs + (escape_sequence) @constant.character.escape.cs -; TODO: Interpolations. +; Interpolations within strings. +(interpolated_string_expression + (interpolation) @meta.embedded.block.cs + (#match? @meta.embedded.block.cs "\\n") + (#set! capture.final)) + +(interpolated_string_expression + (interpolation) @meta.embedded.line.cs) + +(interpolation + (interpolation_brace) @punctuation.section.embedded.begin.cs + (#is? test.firstOfType)) +(interpolation + (interpolation_brace) @punctuation.section.embedded.end.cs + (#is? test.lastOfType)) ; COMMENTS @@ -112,38 +149,85 @@ (local_function_statement name: (identifier) @entity.name.function.cs) +(attribute name: _ @entity.other.attribute-name.cs) + +(property_declaration name: _ @entity.name.property.cs) + ; SUPPORT (invocation_expression function: (identifier) @support.other.function.cs) +; All kinds of Foo in `new Foo()`, `new Foo`, new `Foo.Foo()`, etc. +(object_creation_expression + type: (identifier) @support.class.instance.cs + (#set! capture.final)) + +; Mark `type:` fields within an object creation… +(object_creation_expression + type: _ @_IGNORE_ + (#set! type_instantiation true) + (#set! capture.final)) + +; …then scope all identifiers within (except generic type arguments). +((identifier) @support.class.instance.cs + (#is-not? test.descendantOfType "type_argument_list") + (#is? test.descendantOfNodeWithData "type_instantiation")) + ; TYPES +; Builtin types like `string`. (predefined_type) @support.storage.type.builtin.cs -(type_argument_list - (identifier) @support.storage.type.cs) - -(generic_name (identifier) @support.storage.type.cs) +; Catch and mark all `type:` fields on things that aren't object creation +; expressions. +(_ type: (_) @_IGNORE_ + (#set! type_annotation true)) +; Type coercion. (as_expression - right: (identifier) @support.storage.type.cs) + right: (_) @_IGNORE_ + (#set! type_annotation true)) +; Type checking/binding. (is_expression - right: (identifier) @support.storage.type.cs) + right: (_) @_IGNORE_ + (#set! type_annotation true)) + +; e.g., `value is Foo.Bar`. +(is_pattern_expression + pattern: _ @_IGNORE_ + (#set! type_annotation true)) + +; Generally, anything with a `returns:` field should be highlighted like a type. +(_ + returns: (_) @_IGNORE_ + (#set! type_annotation true)) + +(class_declaration + (base_list) @_IGNORE_ + (#set! type_annotation true)) + +; Scope all identifiers as types when they match those marked nodes… +((identifier) + @support.storage.type.cs + (#is? test.rangeWithData "type_annotation") + (#set! capture.final)) -(_ type: (identifier) @support.storage.type.cs) +; or when they descend from those marked nodes. +((identifier) + @support.storage.type.cs + (#is? test.descendantOfNodeWithData "type_annotation") + (#set! capture.final)) ; TODO: This might be overbroad. -(base_list (identifier) @support.storage.type.cs) +; (base_list (identifier) @support.storage.type.cs) +; Generally, anything with a `returns:` field should be highlighted like a type. (_ returns: (identifier) @support.storage.type.cs (#set! capture.shy)) -(_ - returns: (qualified_name - name: (identifier) @support.storage.type.cs)) ; VARIABLES @@ -158,6 +242,8 @@ (assignment_expression left: (identifier) @variable.other.assignment.cs) +(declaration_pattern name: (identifier) @variable.other.assignment.cs) + (type_parameter_list (type_parameter name: (identifier) @support.storage.type.parameter.cs)) @@ -168,9 +254,29 @@ (enum_member_declaration (identifier) @variable.other.property.cs) +; The `EndsWith` in 'someString.EndsWith("foo")'. +(invocation_expression + (member_access_expression + name: (identifier) @support.other.function.method.cs)) + (#set! capture.final) + +(invocation_expression + (generic_name + (identifier) @support.other.function.cs)) + +; The `Sort` in "Array.Sort". +(invocation_expression + (member_access_expression + name: (generic_name (identifier) @support.other.function.method.cs)) + (#set! capture.final)) + ; The "X" in `ptr->X`. (member_access_expression - name: (identifier) @variable.other.property.cs) + name: (identifier) @variable.other.property.cs + ; This way it won't apply if we've already scoped it as a method call. + (#set! capture.shy)) + +(catch_declaration name: _ @variable.other.assignment.cs) ; KEYWORDS @@ -212,6 +318,8 @@ "unsafe" "with" "stackalloc" + "try" + "switch" ] @keyword.control._TYPE_.cs ( @@ -237,6 +345,8 @@ "?" @keyword.operator.optional.cs ".." @keyword.operator.range.cs +["new"] @keyword.operator._TYPE_.cs + (prefix_unary_expression ["&" "^" "+" "-"] @keyword.operator.unary.cs) @@ -299,6 +409,25 @@ ">>>=" ] @keyword.operator.bitwise.compound.cs +((type_parameter_list + "<" @punctuation.definition.parameters.begin.bracket.angle.cs + ">" @punctuation.definition.parameters.end.bracket.angle.cs) + (#set! capture.final)) + +((type_argument_list + "<" @punctuation.definition.parameters.begin.bracket.angle.cs + ">" @punctuation.definition.parameters.end.bracket.angle.cs) + (#set! capture.final)) + +[ + "==" + "!=" + ">=" + "<=" + ">" + "<" +] @keyword.operator.comparison.cs + (destructor_declaration "~" @keyword.operator.destructor.cs) ; PUNCTUATION @@ -323,13 +452,3 @@ (#set! capture.shy)) (")" @punctuation.definition.end.bracket.round.cs (#set! capture.shy)) - -(type_parameter_list - "<" @punctuation.definition.parameters.begin.bracket.angle.cs - ">" @punctuation.definition.parameters.end.bracket.angle.cs -) - -(type_argument_list - "<" @punctuation.definition.parameters.begin.bracket.angle.cs - ">" @punctuation.definition.parameters.end.bracket.angle.cs -) diff --git a/packages/language-csharp/spec/fixtures/sample.cs b/packages/language-csharp/spec/fixtures/sample.cs index a529d92381..7cec5fac25 100644 --- a/packages/language-csharp/spec/fixtures/sample.cs +++ b/packages/language-csharp/spec/fixtures/sample.cs @@ -29,9 +29,34 @@ static void Main(string[] args) { Car myCar = new Car(); // ^^^ support.storage.type.cs // ^^^^^ variable.other.assignment.cs +// ^^^ keyword.operator.new.cs myCar.honk(); Console.WriteLine(myCar.brand + " " + myCar.modelName); +// ^^^^^^^^^ support.other.function.method.cs + + Array.Sort(myNumbers); +// ^^^^ support.other.function.method.cs +// ^^^ support.storage.type.builtin.cs + + Console.WriteLine($"Doing {noun}..."); +// ^^^^^^^^^^^^^^^^^^ string.quoted.double.interpolated.cs +// ^ punctuation.section.embedded.begin.cs +// ^ punctuation.section.embedded.end.cs + + string json = $$""" + { + "name": {{name.toUpperCase("foo")}} + } + """; +// ^^^ punctuation.definition.string.end.cs + } } + +static readonly string[] signinMethods = { +// ^^^^^^^^ storage.modifier.readonly.cs + "Credentials", + "QR Code" +}; diff --git a/packages/language-html/grammars/modern-tree-sitter-html.cson b/packages/language-html/grammars/modern-tree-sitter-html.cson index cc36821ecd..7143c9b3d3 100644 --- a/packages/language-html/grammars/modern-tree-sitter-html.cson +++ b/packages/language-html/grammars/modern-tree-sitter-html.cson @@ -5,6 +5,8 @@ parser: 'tree-sitter-html' injectionRegex: '(HTML|html|Html)$' +firstLineRegex: "<(?:!DOCTYPE\\s*)?html|-\\*-(?:\\s*(?=[^:;\\s]+\\s*-\\*-)|(?:.*?[;\\s]|(?<=-\\*-))mode\\s*:\\s*)html(?=[\\s;]|(?]?\\d+|m)?|\\sex)(?=:(?=\\s*set?\\s[^\\n:]+:)|:(?!\\s*set?\\s))(?:(?:\\s|\\s*:\\s*)\\w*(?:\\s*=(?:[^\\n\\\\\\s]|\\\\.)*)?)*[\\s:](?:filetype|ft|syntax)\\s*=x?html(?=\\s|:|$)" + treeSitter: parserSource: 'github:tree-sitter/tree-sitter-html#v0.23.0' grammar: 'tree-sitter-html/tree-sitter-html.wasm' diff --git a/packages/symbol-provider-ctags/lib/ctags-provider.js b/packages/symbol-provider-ctags/lib/ctags-provider.js index 6d587ce779..cd336db763 100644 --- a/packages/symbol-provider-ctags/lib/ctags-provider.js +++ b/packages/symbol-provider-ctags/lib/ctags-provider.js @@ -163,7 +163,7 @@ class CtagsProvider { } getLanguage(editor) { - if (['.cson', '.gyp'].includes(path.extname(this.path))) { + if (['.cson', '.gyp'].includes(path.extname(this.getPath()))) { return 'Cson'; } diff --git a/src/wasm-tree-sitter-language-mode.js b/src/wasm-tree-sitter-language-mode.js index 1e4a7a4e57..39bf0f6ff7 100644 --- a/src/wasm-tree-sitter-language-mode.js +++ b/src/wasm-tree-sitter-language-mode.js @@ -29,10 +29,6 @@ const PARSERS_IN_USE = new Set(); const FUNCTION_TRUE = () => true; -function isParseTimeout(err) { - return err.message.includes('Parsing failed'); -} - function last(array) { return array[array.length - 1]; } @@ -180,6 +176,15 @@ function isBetweenPoints(point, a, b) { comparePoints(point, greater) <= 0; } +// Make a function suitable for passing to `Parser::parse` as a progress callback. +function makeProgressCallback (syncTimeoutMicros = PARSE_JOB_LIMIT_MICROS) { + let start = performance.now() * 1000; + return () => { + let now = performance.now() * 1000; + return (now - start) >= syncTimeoutMicros; + }; +} + // eslint-disable-next-line no-unused-vars let totalBufferChanges = 0; let nextTransactionId = 1; @@ -223,6 +228,11 @@ class WASMTreeSitterLanguageMode { this.grammarForLanguageString = this.grammarForLanguageString.bind(this); + // Keep track of parsers by language. This is a `Map` whose keys are + // `Language` instances and whose values are arrays of `Parser` instances. + // When we need to parse in a certain language, we take the first instance + // for that language that is not currently in use by another parsing job, + // or create a new `Parser` if there isn't one available. this.parsersByLanguage = new Index(); this.tokenIterator = new TokenIterator(this); @@ -248,7 +258,9 @@ class WASMTreeSitterLanguageMode { // considers only a single `LanguageLayer` at a time. For instance, a given // indentation task might consult one layer's indentation query to know // whether to indent a line, but another layer's indentation query to know - // whether to dedent the line. There are no simplicity gains to be made. + // whether to dedent the line. There are no simplicity gains to be made + // from having each `LanguageLayer` declare its own instance of + // `IndentResolver`. // // `IndentResolver` _could_ therefore fold its methods into // `WASMTreeSitterLanguageMode`, but is separate from it for reasons of @@ -308,6 +320,9 @@ class WASMTreeSitterLanguageMode { parser = pool.find(p => !PARSERS_IN_USE.has(p)); } + // We try to find the first `Parser` instance of the given language that is + // not currently being used. If we fail, we'll create a new parser and add + // it to the pool for future use. if (!parser) { parser = new Parser(); parser.setLanguage(language); @@ -759,11 +774,27 @@ class WASMTreeSitterLanguageMode { return point; } + // Private: Parse the buffer with the given language. + // + // Will try to parse synchronously, but will go async if the time budget for + // parsing is exceeded. + // + // * `language`: A Tree-sitter {Language} instance. + // * `oldTree`: Optional; the last {Tree} that was produced from parsing. + // Helps reduce parsing time if given because Tree-sitter can re-use the + // tree and make incremental changes as needed. + // * `includedRanges`: Optional; an {Array} of objects that contain + // `startIndex`, `endIndex`, `startPosition`, and `endPosition` keys. Tells + // Tree-sitter to consider only those ranges of the document and ignore + // everything else. Used when parsing injections. + // + // Returns either a {Tree} (if parsing was able to be achieved synchronously) + // or a {Promise} that eventually resolves with a {Tree}. + // parseAsync(language, oldTree, includedRanges, { tag = null } = {}) { let devMode = atom.inDevMode(); let parser = this.getOrCreateParserForLanguage(language); parser.reset(); - parser.setTimeoutMicros(this.syncTimeoutMicros); PARSERS_IN_USE.add(parser); // When you edit a tree, the positions of nodes in the tree are adjusted @@ -798,6 +829,7 @@ class WASMTreeSitterLanguageMode { let currentText = parseDone ? this.cachedCurrentBufferText : text; return currentText.slice(index, endIndex); }; + let progressCallback = makeProgressCallback(this.syncTimeoutMicros); let tree; // eslint-disable-next-line no-unused-vars @@ -811,27 +843,54 @@ class WASMTreeSitterLanguageMode { console.log(`(async: ${batchCount} batches)`); } } - parser.setTimeoutMicros(null); PARSERS_IN_USE.delete(parser); }; if (devMode && tag) { console.time(tag); } - try { - // Attempt a synchronous parse. - tree = parser.parse(callback, oldTree, { includedRanges }); - } catch (err) { - if (!isParseTimeout(err)) { throw err; } + // Attempt a synchronous parse. + tree = parser.parse(callback, oldTree, { includedRanges, progressCallback }); - // The parse couldn't be completed in the allotted time, so we'll go - // async and return a promise. + // The tree can be `null` if we never set a language on the `Parser` + // instance — but we definitely did that. So if it's `null` after calling + // `parse`, it can only be for the other reason: parsing could not finish + // in the allotted time. If that happens, we'll go async and return a + // promise. + if (tree === null) { return new Promise((resolve, reject) => { + // Perform however many jobs (each of no more than `syncTimeoutMicros` + // duration) are needed to finish parsing this document. We yield after + // each job so that we don't lock up the UI. + // + // NOTE: If X equals the job time limit and Y equals how long we yield + // in between jobs, it's possible that there are "sweet-spot" values of + // X and Y that strike the right balance between performance and prompt + // parsing. + // + // By default, the job time limit is 3000 microseconds, or 3ms. Using + // `setImmediate` to reschedule results in a very low value for Y. On + // large files, this winds up prioritizing parsing work quite a bit + // while still guaranteeing that the UI won't completely lock up. + // + // But we could try an approach like this instead: + // + // An animation frame is about 16ms long (usually; depends on the + // display). So, for example, we could switch from `setImmediate` to + // `requestAnimationFrame` — in which case we'd be saying that we want + // to spend no more than 3/16ths of the animation frame (18.75%) on + // parsing the buffer. That would represent an increase in Y from what + // we're doing now, so it might also be worth it to increase X. + // const parseJob = () => { try { batchCount++; - tree = parser.parse(callback, oldTree, { includedRanges }); + tree = parser.parse(callback, oldTree, { includedRanges, progressCallback }); } catch (err) { - if (!isParseTimeout(err)) { return reject(err); } + return reject(err); + } + + if (tree === null) { + // Still not done! setImmediate(parseJob); return; } @@ -848,11 +907,17 @@ class WASMTreeSitterLanguageMode { return tree; } + // Private: Parse the buffer with the given language. + // + // Always synchronous, unlike {::parseAsync}. This will be used if the + // `useAsyncParsing` property on {WasmTreeSitterLanguageMode} is `false`; but + // that's meant to be changed _only_ when running certain specs. + // + // Do not use this method in non-spec scenarios; prefer {::parseAsync}. parse(language, oldTree, includedRanges, { tag = null } = {}) { let devMode = atom.inDevMode(); let parser = this.getOrCreateParserForLanguage(language); parser.reset(); - parser.setTimeoutMicros(null); let text = this.buffer.getText(); this.cachedCurrentBufferText = text;