diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs index 67fb0a15..74aeccce 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -31,89 +32,42 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices private static Regex ValidSqlNameRegex = new Regex(@"^[\p{L}_][\p{L}\p{N}@$#_]{0,127}$"); + private static CompletionItem[] emptyCompletionList = new CompletionItem[0]; + private static readonly string[] DefaultCompletionText = new string[] - { - "absolute", - "accent_sensitivity", - "action", - "activation", - "add", - "address", - "admin", - "after", - "aggregate", - "algorithm", - "allow_page_locks", - "allow_row_locks", - "allow_snapshot_isolation", + { + "all", "alter", - "always", - "ansi_null_default", - "ansi_nulls", - "ansi_padding", - "ansi_warnings", - "application", - "arithabort", + "and", + "apply", "as", "asc", - "assembly", - "asymmetric", "at", - "atomic", - "audit", - "authentication", - "authorization", - "auto", - "auto_close", - "auto_shrink", - "auto_update_statistics", - "auto_update_statistics_async", - "availability", "backup", - "before", "begin", "binary", "bit", - "block", "break", - "browse", - "bucket_count", "bulk", "by", "call", - "caller", - "card", "cascade", "case", - "catalog", "catch", - "change_tracking", - "changes", "char", "character", "check", "checkpoint", "close", "clustered", - "collection", "column", - "column_encryption_key", "columnstore", "commit", - "compatibility_level", - "compress_all_row_groups", - "compression", - "compression_delay", - "compute", - "concat_null_yields_null", - "configuration", "connect", "constraint", - "containstable", "continue", "create", - "cube", - "current", + "cross", "current_date", "cursor", "cursor_close_on_commit", @@ -122,45 +76,31 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "data_compression", "database", "date", - "date_correlation_optimization", - "datefirst", "datetime", "datetime2", "days", - "db_chaining", "dbcc", - "deallocate", "dec", "decimal", "declare", "default", - "delayed_durability", "delete", "deny", "desc", "description", - "disable_broker", "disabled", "disk", "distinct", - "distributed", "double", "drop", "drop_existing", "dump", - "durability", "dynamic", "else", "enable", "encrypted", - "encryption_type", "end", "end-exec", - "entry", - "errlvl", - "escape", - "event", - "except", "exec", "execute", "exit", @@ -171,20 +111,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "filegroup", "filename", "filestream", - "fillfactor", "filter", "first", "float", "for", "foreign", - "freetext", - "freetexttable", "from", "full", - "fullscan", - "fulltext", "function", - "generated", "geography", "get", "global", @@ -200,30 +134,26 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "holdlock", "hours", "identity", - "identity_insert", "identitycol", "if", - "ignore_dup_key", "image", "immediate", "include", "index", - "inflectional", - "insensitive", + "inner", "insert", "instead", "int", "integer", - "integrated", "intersect", "into", "isolation", + "join", "json", "key", - "kill", "language", "last", - "legacy_cardinality_estimation", + "left", "level", "lineno", "load", @@ -232,16 +162,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "location", "login", "masked", - "master", "maxdop", - "memory_optimized", "merge", "message", "modify", "move", - "multi_user", "namespace", - "national", "native_compilation", "nchar", "next", @@ -252,8 +178,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "none", "norecompute", "now", + "null", "numeric", - "numeric_roundabort", "object", "of", "off", @@ -261,21 +187,15 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "on", "online", "open", - "opendatasource", - "openquery", "openrowset", "openxml", "option", + "or", "order", "out", "output", "over", "owner", - "pad_index", - "page", - "page_verify", - "parameter_sniffing", - "parameterization", "partial", "partition", "password", @@ -286,7 +206,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "persisted", "plan", "policy", - "population", "precision", "predicate", "primary", @@ -295,7 +214,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "proc", "procedure", "public", - "query_optimizer_hotfixes", "query_store", "quoted_identifier", "raiserror", @@ -318,7 +236,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "relative", "remove", "reorganize", - "replication", "required", "restart", "restore", @@ -328,7 +245,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "returns", "revert", "revoke", - "role", "rollback", "rollup", "row", @@ -344,11 +260,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "scroll", "secondary", "security", - "securityaudit", "select", - "semantickeyphrasetable", - "semanticsimilaritydetailstable", - "semanticsimilaritytable", "send", "sent", "sequence", @@ -357,12 +269,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "set", "sets", "setuser", - "shutdown", "simple", "smallint", "smallmoney", "snapshot", - "sort_in_tempdb", "sql", "standard", "start", @@ -374,20 +284,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "statistics_norecompute", "status", "stopped", - "supported", - "symmetric", "sysname", "system", "system_time", - "system_versioning", "table", - "tablesample", "take", "target", - "textimage_on", - "textsize", "then", - "thesaurus", "throw", "time", "timestamp", @@ -398,14 +301,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "transaction", "trigger", "truncate", - "trustworthy", "try", "tsql", "type", + "uncommitted", "union", "unique", "uniqueidentifier", - "unlimited", "updatetext", "use", "user", @@ -413,24 +315,32 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "value", "values", "varchar", - "varying", "version", "view", "waitfor", - "weight", "when", "where", "while", "with", "within", - "within group", "without", "writetext", "xact_abort", "xml", - "zone" }; + /// + /// Gets a static instance of an empty completion list to avoid + // unneeded memory allocations + /// + internal static CompletionItem[] EmptyCompletionList + { + get + { + return AutoCompleteHelper.emptyCompletionList; + } + } + /// /// Gets or sets the current workspace service instance /// Setter for internal testing purposes only @@ -449,7 +359,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { AutoCompleteHelper.workspaceServiceInstance = value; } - } + } /// /// Get the default completion list from hard-coded list @@ -462,17 +372,47 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices int row, int startColumn, int endColumn, - bool useLowerCase) + bool useLowerCase, + string tokenText = null) { - var completionItems = new CompletionItem[DefaultCompletionText.Length]; - for (int i = 0; i < DefaultCompletionText.Length; ++i) + // determine how many default completion items there will be + int listSize = DefaultCompletionText.Length; + if (!string.IsNullOrWhiteSpace(tokenText)) { - completionItems[i] = CreateDefaultCompletionItem( - useLowerCase ? DefaultCompletionText[i].ToLower() : DefaultCompletionText[i].ToUpper(), - row, - startColumn, - endColumn); + listSize = 0; + foreach (var completionText in DefaultCompletionText) + { + if (completionText.StartsWith(tokenText, StringComparison.OrdinalIgnoreCase)) + { + ++listSize; + } + } } + + // special case empty list to avoid unneed array allocations + if (listSize == 0) + { + return emptyCompletionList; + } + + // build the default completion list + var completionItems = new CompletionItem[listSize]; + int completionItemIndex = 0; + foreach (var completionText in DefaultCompletionText) + { + // add item to list if the tokenText is null (meaning return whole list) + // or if the completion item begins with the tokenText + if (string.IsNullOrWhiteSpace(tokenText) || completionText.StartsWith(tokenText, StringComparison.OrdinalIgnoreCase)) + { + completionItems[completionItemIndex] = CreateDefaultCompletionItem( + useLowerCase ? completionText.ToLower() : completionText.ToUpper(), + row, + startColumn, + endColumn); + ++completionItemIndex; + } + } + return completionItems; } @@ -559,8 +499,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices int row, int startColumn, int endColumn) - { - + { List completions = new List(); foreach (var autoCompleteItem in suggestions) @@ -596,8 +535,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices completions.Add(CreateCompletionItem(autoCompleteItem.Title, autoCompleteItem.Title, insertText, kind, row, startColumn, endColumn)); } - - return completions.ToArray(); } @@ -636,6 +573,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices QueueItem queueItem = bindingQueue.QueueBindingOperation( key: scriptInfo.ConnectionKey, bindingTimeout: AutoCompleteHelper.PrepopulateBindTimeout, + waitForLockTimeout: AutoCompleteHelper.PrepopulateBindTimeout, bindOperation: (bindingContext, cancelToken) => { // parse a simple statement that returns common metadata diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs index 29310269..6058ec42 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs @@ -61,7 +61,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices string key, Func bindOperation, Func timeoutOperation = null, - int? bindingTimeout = null) + int? bindingTimeout = null, + int? waitForLockTimeout = null) { // don't add null operations to the binding queue if (bindOperation == null) @@ -74,7 +75,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Key = key, BindOperation = bindOperation, TimeoutOperation = timeoutOperation, - BindingTimeout = bindingTimeout + BindingTimeout = bindingTimeout, + WaitForLockTimeout = waitForLockTimeout }; lock (this.bindingQueueLock) @@ -198,7 +200,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices int bindTimeout = queueItem.BindingTimeout ?? bindingContext.BindingTimeout; // handle the case a previous binding operation is still running - if (!bindingContext.BindingLock.WaitOne(0)) + if (!bindingContext.BindingLock.WaitOne(queueItem.WaitForLockTimeout ?? 0)) { queueItem.Result = queueItem.TimeoutOperation != null ? queueItem.TimeoutOperation(bindingContext) @@ -266,8 +268,15 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } finally { - // reset the item queued event since we've processed all the pending items - this.itemQueuedEvent.Reset(); + lock (this.bindingQueueLock) + { + // verify the binding queue is still empty + if (this.bindingQueue.Count == 0) + { + // reset the item queued event since we've processed all the pending items + this.itemQueuedEvent.Reset(); + } + } } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index a5d27b52..dba3d0aa 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -583,28 +583,45 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// internal CompletionItem ResolveCompletionItem(CompletionItem completionItem) { - try + var scriptParseInfo = LanguageService.Instance.currentCompletionParseInfo; + if (scriptParseInfo != null && scriptParseInfo.CurrentSuggestions != null) { - var scriptParseInfo = LanguageService.Instance.currentCompletionParseInfo; - if (scriptParseInfo != null && scriptParseInfo.CurrentSuggestions != null) + if (Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock)) { - foreach (var suggestion in scriptParseInfo.CurrentSuggestions) + try { - if (string.Equals(suggestion.Title, completionItem.Label)) - { - completionItem.Detail = suggestion.DatabaseQualifiedName; - completionItem.Documentation = suggestion.Description; - break; - } + QueueItem queueItem = this.BindingQueue.QueueBindingOperation( + key: scriptParseInfo.ConnectionKey, + bindingTimeout: LanguageService.BindingTimeout, + bindOperation: (bindingContext, cancelToken) => + { + foreach (var suggestion in scriptParseInfo.CurrentSuggestions) + { + if (string.Equals(suggestion.Title, completionItem.Label)) + { + completionItem.Detail = suggestion.DatabaseQualifiedName; + completionItem.Documentation = suggestion.Description; + break; + } + } + return completionItem; + }); + + queueItem.ItemProcessed.WaitOne(); } + catch (Exception ex) + { + // if any exceptions are raised looking up extended completion metadata + // then just return the original completion item + Logger.Write(LogLevel.Error, "Exeception in ResolveCompletionItem " + ex.ToString()); + } + finally + { + Monitor.Exit(scriptParseInfo.BuildingMetadataLock); + } } } - catch (Exception ex) - { - // if any exceptions are raised looking up extended completion metadata - // then just return the original completion item - Logger.Write(LogLevel.Error, "Exeception in ResolveCompletionItem " + ex.ToString()); - } + return completionItem; } @@ -674,27 +691,32 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices ScriptFile scriptFile, ConnectionInfo connInfo) { + // initialize some state to parse and bind the current script file + this.currentCompletionParseInfo = null; + CompletionItem[] resultCompletionItems = null; string filePath = textDocumentPosition.TextDocument.Uri; int startLine = textDocumentPosition.Position.Line; + int parserLine = textDocumentPosition.Position.Line + 1; int startColumn = TextUtilities.PositionOfPrevDelimeter( scriptFile.Contents, textDocumentPosition.Position.Line, textDocumentPosition.Position.Character); - int endColumn = textDocumentPosition.Position.Character; + int endColumn = TextUtilities.PositionOfNextDelimeter( + scriptFile.Contents, + textDocumentPosition.Position.Line, + textDocumentPosition.Position.Character); + int parserColumn = textDocumentPosition.Position.Character + 1; bool useLowerCaseSuggestions = this.CurrentSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value; - this.currentCompletionParseInfo = null; - CompletionItem[] defaultCompletionItems = AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions); - CompletionItem[] resultCompletionItems = defaultCompletionItems; - CompletionItem[] emptyCompletionItems = new CompletionItem[0]; - int line = textDocumentPosition.Position.Line + 1; - int column = textDocumentPosition.Position.Character + 1; - // get the current script parse info object ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri); - if (connInfo == null || scriptParseInfo == null) + if (scriptParseInfo == null) { - return defaultCompletionItems; + return AutoCompleteHelper.GetDefaultCompletionItems( + startLine, + startColumn, + endColumn, + useLowerCaseSuggestions); } // reparse and bind the SQL statement if needed @@ -703,14 +725,23 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices ParseAndBind(scriptFile, connInfo); } + // if the parse failed then return the default list if (scriptParseInfo.ParseResult == null) { - return defaultCompletionItems; + return AutoCompleteHelper.GetDefaultCompletionItems( + startLine, + startColumn, + endColumn, + useLowerCaseSuggestions); } - Token token = GetToken(scriptParseInfo, line, column); + + // need to adjust line & column for base-1 parser indices + Token token = GetToken(scriptParseInfo, parserLine, parserColumn); + string tokenText = token != null ? token.Text : null; + // check if the file is connected and the file lock is available if (scriptParseInfo.IsConnected && Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock)) - { + { try { // queue the completion task with the binding queue @@ -719,33 +750,35 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices bindingTimeout: LanguageService.BindingTimeout, bindOperation: (bindingContext, cancelToken) => { - CompletionItem[] completions = null; - // get the completion list from SQL Parser scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions( scriptParseInfo.ParseResult, - line, - column, + parserLine, + parserColumn, bindingContext.MetadataDisplayInfoProvider); // cache the current script parse info object to resolve completions later this.currentCompletionParseInfo = scriptParseInfo; // convert the suggestion list to the VS Code format - completions = AutoCompleteHelper.ConvertDeclarationsToCompletionItems( + return AutoCompleteHelper.ConvertDeclarationsToCompletionItems( scriptParseInfo.CurrentSuggestions, startLine, startColumn, - endColumn - ); - - return completions; + endColumn); }, timeoutOperation: (bindingContext) => { - return defaultCompletionItems; + // return the default list if the connected bind fails + return AutoCompleteHelper.GetDefaultCompletionItems( + startLine, + startColumn, + endColumn, + useLowerCaseSuggestions, + tokenText); }); + // wait for the queue item queueItem.ItemProcessed.WaitOne(); var completionItems = queueItem.GetResultAsT(); @@ -755,7 +788,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } else if (!ShouldShowCompletionList(token)) { - resultCompletionItems = emptyCompletionItems; + resultCompletionItems = AutoCompleteHelper.EmptyCompletionList; } } finally @@ -763,7 +796,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Monitor.Exit(scriptParseInfo.BuildingMetadataLock); } } - //resultCompletionItems = AutoCompleteHelper.AddTokenToItems(resultCompletionItems, token, startLine, startColumn, endColumn); + + // if there are no completions then provide the default list + if (resultCompletionItems == null) + { + resultCompletionItems = AutoCompleteHelper.GetDefaultCompletionItems( + startLine, + startColumn, + endColumn, + useLowerCaseSuggestions, + tokenText); + } + return resultCompletionItems; } @@ -774,7 +818,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices var tokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.FindToken(startLine, startColumn); if (tokenIndex >= 0) { - return scriptParseInfo.ParseResult.Script.Tokens.ToList()[tokenIndex]; + // return the current token + int currentIndex = 0; + foreach (var token in scriptParseInfo.ParseResult.Script.Tokens) + { + if (currentIndex == tokenIndex) + { + return token; + } + ++currentIndex; + } } } return null; @@ -930,6 +983,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // Get the requested files foreach (ScriptFile scriptFile in filesToAnalyze) { + if (IsPreviewWindow(scriptFile)) + { + continue; + } + Logger.Write(LogLevel.Verbose, "Analyzing script file: " + scriptFile.FilePath); ScriptFileMarker[] semanticMarkers = GetSemanticMarkers(scriptFile); Logger.Write(LogLevel.Verbose, "Analysis complete."); diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs index 931bf524..a320f842 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs @@ -51,6 +51,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public int? BindingTimeout { get; set; } + /// + /// Gets or sets the timeout for how long to wait for the binding lock + /// + public int? WaitForLockTimeout { get; set; } + /// /// Converts the result of the execution to type T /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/TextUtilities.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/TextUtilities.cs index 0da84f43..29c9fabc 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Utility/TextUtilities.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/TextUtilities.cs @@ -6,7 +6,34 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility { public static class TextUtilities - { + { + /// + /// Find the position of the cursor in the SQL script content buffer and return previous new line position + /// + /// + /// + /// + /// + public static int PositionOfCursor(string sql, int startRow, int startColumn, out int prevNewLine) + { + prevNewLine = 0; + if (string.IsNullOrWhiteSpace(sql)) + { + return 1; + } + + for (int i = 0; i < startRow; ++i) + { + while (prevNewLine < sql.Length && sql[prevNewLine] != '\n') + { + ++prevNewLine; + } + ++prevNewLine; + } + + return startColumn + prevNewLine; + } + /// /// Find the position of the previous delimeter for autocomplete token replacement. /// SQL Parser may have similar functionality in which case we'll delete this method. @@ -14,49 +41,70 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility /// /// /// - /// + /// public static int PositionOfPrevDelimeter(string sql, int startRow, int startColumn) - { - if (string.IsNullOrWhiteSpace(sql)) - { - return 1; - } + { + int prevNewLine; + int delimeterPos = PositionOfCursor(sql, startRow, startColumn, out prevNewLine); - int prevLineColumns = 0; - for (int i = 0; i < startRow; ++i) + if (delimeterPos - 1 < sql.Length) { - while (sql[prevLineColumns] != '\n' && prevLineColumns < sql.Length) + while (--delimeterPos >= prevNewLine) { - ++prevLineColumns; - } - ++prevLineColumns; - } - - startColumn += prevLineColumns; - - if (startColumn - 1 < sql.Length) - { - while (--startColumn >= prevLineColumns) - { - if (sql[startColumn] == ' ' - || sql[startColumn] == '\t' - || sql[startColumn] == '\n' - || sql[startColumn] == '.' - || sql[startColumn] == '+' - || sql[startColumn] == '-' - || sql[startColumn] == '*' - || sql[startColumn] == '>' - || sql[startColumn] == '<' - || sql[startColumn] == '=' - || sql[startColumn] == '/' - || sql[startColumn] == '%') + if (IsCharacterDelimeter(sql[delimeterPos])) { break; } } + + delimeterPos = delimeterPos + 1 - prevNewLine; } - return startColumn + 1 - prevLineColumns; + return delimeterPos; + } + + /// + /// Find the position of the next delimeter for autocomplete token replacement. + /// + /// + /// + /// + public static int PositionOfNextDelimeter(string sql, int startRow, int startColumn) + { + int prevNewLine; + int delimeterPos = PositionOfCursor(sql, startRow, startColumn, out prevNewLine); + + while (delimeterPos < sql.Length) + { + if (IsCharacterDelimeter(sql[delimeterPos])) + { + break; + } + ++delimeterPos; + } + + return delimeterPos - prevNewLine; + } + + /// + /// Determine if the character is a SQL token delimiter + /// + /// + private static bool IsCharacterDelimeter(char ch) + { + return ch == ' ' + || ch == '\t' + || ch == '\n' + || ch == '.' + || ch == '+' + || ch == '-' + || ch == '*' + || ch == '>' + || ch == '<' + || ch == '=' + || ch == '/' + || ch == '%' + || ch == ','; } } }