La Biblia Del SynEdit - Rev7
La Biblia Del SynEdit - Rev7
La Biblia Del SynEdit - Rev7
Lazarus
La Biblia del SynEdit
1 de 153 15/01/2018
Lazarus La Biblia del SynEdit
FICHA TÉCNICA
2 de 153 15/01/2018
Lazarus La Biblia del SynEdit
CONTROL DE CAMBIOS
3 de 153 15/01/2018
Lazarus La Biblia del SynEdit
4 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Gran parte de este trabajo está basado en la experiencia de uso, la escasa documentación que
existe en la web, Ingeniería inversa, y al análisis del código fuente del componente SynEdit.
Para ser exactos, SynEdit es todo un paquete que viene ya integrado en Lazarus cuando se
instala (y que incluye diversos componentes), pero por lo general, cuando decimos SynEdit, nos
referimos al componente TSynEdit que es el editor con posibilidades de resaltado de sintaxis.
• TSynMemo.- Versión de TSynEdit con algunas diferencias. Tiene menos métodos y eventos
publicados. Deriva de SynEdit. Puede remplazar a SynEdit, en muchos casos.
• etc.
El control SynEdit, que se incluye en Lazarus, es una versión modificada del proyecto
independiente SynEdit. La versión adaptada para Lazarus, se ha desarrollado a partir de la versión 1.03,
5 de 153 15/01/2018
Lazarus La Biblia del SynEdit
a la que se le ha agregado algunas características adicionales, como soporte para UTF-8 y Plegado de
código.
Este componente, está bien revisado y comprobado, ya que es el mismo que usa el IDE de
Lazarus para su Editor de Código.
• Soporta coloreado de sintaxis, para varios lenguajes predefinidos o se le puede crear una nueva
sintaxis.
1.3 Apariencia
6 de 153 15/01/2018
Lazarus La Biblia del SynEdit
La principal diferencia visual está en la barra vertical que aparece a la izquierda. Esta barra sirve
para mostrar el número de línea, y para otras opciones más. Otra diferencia es que el tamaño horizontal
de letra es uniforme. Es decir que la letra “m”, tiene el mismo ancho que la letra “l”. Este es el tipo de
letra que se carga por defecto en un “SynEdit”.
Este resaltado consiste, por defecto, en poner los caracteres inicial y final, en modo negrita.
Si se desea modificar el atributo del resaltado de los delimitadores, se puede usar el siguiente
código:
Otra de las características que viene por defecto en SynEdit, es la opción de poder crear
marcadores (Ver 1.8.5 - Marcadores de texto). SI no se va a usar, esta opción se debe deshabilitarla
porque podría generar errores en tiempo de ejecución.
También las opciones de Cortar, Copiar y Pegar, se encuentran habilitadas por defecto, en el
control SynEdit, sin necesidad de implementarlas.
En general, todos los atajos que se crean por defecto en SynEdit, corresponden a acciones que
están predefinidas sin necesidad de activarlas.
7 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Existen diversas propiedades para cambiar la apariencia del control SynEdit. Describiremos
algunas de ellas.
El panel vertical que aparece en la parte izquierda del control, es llamada “Gutter” (canal), y es
la destinada a mostrar el número de línea, las marcas de plegado (folding), las marcas de cambio y los
marcadores.
Número de
línea
Marcadores
Marcas de
cambio
Marcas de
“folding”
Gutter
El “Gutter”, se puede mostrar u ocultar por código. Para hacerlo invisible se debería hacer:
SynEdit1.Gutter.Visible := False;
En este caso, nuestro editor tiene el nombre por defecto que se le asigna al agregarlo a un
formulario: SynEdit1.
El “Gutter”, por defecto, tiene el ancho ajustado de forma automática, es decir que cambia, de
acuerdo a la cantidad de filas en el editor. Se le puede fijar a un ancho determinado, poniendo primero
la propiedad “Autosize” a “false”:
ed.Gutter.AutoSize:=false;
ed.Gutter.Width:=30;
No es recomendable cambiar así, el ancho, porque de esta forma, no se reubican los elementos
que contiene, así que se podría perder de vista parte de los números o las marcas de “folding”.
Es preferible dejar el “Autosize” en “true” y desactivar elementos individuales del “Gutter”, para
variar su tamaño. Esto se puede hacer fácilmente con el inspector de objetos, modificando la propiedad
“Parts”, de la propiedad “Gutter”:
8 de 153 15/01/2018
Lazarus La Biblia del SynEdit
En este ejemplo se oculta el área destinada a los marcadores. Al ir ocultando áreas, el tamaño
total del “Gutter”, va disminuyendo.
Por defecto en SynEdit aparece una línea vertical en la parte derecha del texto, usualmente en la
columna 80. Esta línea sirve de ayuda para cuando se quiera imprimir el contenido y se desee evitar
sobrepasarse en el tamaño de la línea que permite la impresora.
9 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Si no se desea que esta línea aparezca, se puede poner su posición en una coordenada negativa:
Las barras de desplazamiento del editor se pueden ocultar o mostrar usando la propiedad
“ScrollBars”.
1.3.3 Tipografía
SynEdit permite configurar diversas propiedades de la tipografía a usar. Por defecto el texto se
muestra con la fuente “Courirer New” en tamaño 10.
Para cambia la fuente que usará sin SynEdit se debe configurar el objeto Font. Esta tarea se
puede hacer por código o usando el inspector de objetos:
10 de 153 15/01/2018
Lazarus La Biblia del SynEdit
El objeto Font, tiene diversas propiedades y métodos, muchos de los cuales, solo pueden ser
accedidos por código.
Quizá la propiedad más común, para cambiar la apariencia del texto en pantalla es la fuente.
Esta se puede cambiar mediante la propiedad “Name”.
Considerar que los caracteres a mostrar en SynEdit son siempre monoespaciados, es decir que
todos los caracteres tendrán el mismo ancho en la pantalla. Si se usará una fuente con ancho distinto
para cada carácter, SynEdit lo mostrará igual como una fuente monoespaciada, dando la impresión de
que los caracteres no están igualmente espaciados.
El siguiente ejemplo muestra un editor en el que se ha usado el tipo de letra “Arial”, que no es
monoespaciada:
El texto ha sido correctamente escrito y separado, pero como las letras en “Arial”, tienen ancho
distinto (la “m” es más ancha que la “i”), el texto da la apriencia de estar mal espaciado. Debido a este
11 de 153 15/01/2018
Lazarus La Biblia del SynEdit
efecto, se recomienda usar solo los tipos de letra que son de ancho uniforme, como Courier, Fixed, o
Lucida.
También es posible cambiar el espaciado entre líneas y entre caracteres, usando las propiedades
“ExtraCharSpacing” y “ExtraLineSpacing”:
SynEdit1.ExtraCharSpacing:=5;
SynEdit1.ExtraLineSpacing:=10;
Por defecto el espaciado es cero. Si se desea juntar, en vez de separar, se pueden usar valores
negativos para estas propiedades.
Otra de las propiedades que podemos usar para personalizar a SynEdit, es el juego de caracteres
(CharSet).
El juego de caracteres permite cambiar el idioma a usar en el editor. Por ejemplo, usando el
juego de caracteres CHINESEBIG5_CHARSET, podríamos usar caracteres chinos:
Algunos juegos de caracteres, como el del ejemplo, usan para un caracter, el doble de espacio
en el editor, que los caracteres tradicionales occidentales. No hay problema, SynEdit, puede manejar
este tipo de caracteres e inclusive combinar caracteres de espacio simple y doble (o de dos juegos
distintos de caracteres) en el mismo documento.
12 de 153 15/01/2018
Lazarus La Biblia del SynEdit
1.4 Funcionamiento
Para SynEdit, la pantalla es una grilla de celdas, en donde cada celda representa un carácter1:
(1,2) (2,2)
...
Cada celda está
representada por su
(x,y)
coordenada (fila, columna)
Por otro lado, la información a mostrar en pantalla se almacena en una lista de cadenas, donde
cada cadena representa a una línea.
Esta diferencia se hace notoria, sobre todo por el hecho de que el SynEdit maneja codificación
UTF-8, lo que complica el manejo de las coordenadas en pantalla.
Las cadenas se almacenan como secuencias de bytes, pero lo que se muestra en pantalla son
secuencias de caracteres ubicados en celdas.
• Un byte en la cadena puede representar a más de una carácter en pantalla3. Esto es cierto
cuando se usan tabulaciones y están deben ser expandidas en varios espacios.
1
Esto no es del todo cierto, porque algunos caracteres orientales pueden ocupar dos celdas del editor (full width).
2
Inicialmente en los primeros editores de texto, siempre se mantenía una correspondencia de 1 a 1 (excepto
cuando se soportaban tabulaciones), usando la codificación ASCCI o alguna similar.
3
Para complicar las cosas, si consideramos que un carácter en pantalla puede ocupar dos celdas del editor, en
general, uno o más bytes de cadena pueden representar a una o dos celdas en la pantalla.
13 de 153 15/01/2018
Lazarus La Biblia del SynEdit
• Un carácter en pantalla puede ser representado por más de un byte en la cadena. Esto
sucede porque SynEdit, maneja la codificación universal UTF-8, que en algunos casos (como
las vocales acentuadas), asignan más de un byte por carácter.
El siguiente esquema muestra cómo se codifica una cadena de texto típica en SynEdit:
bytes 62 65 67 69 6E 09 63 3D 22 74 C3 BA 22 3B
celdas b e g i n c = " t ú " ;
bytes: $62 $65 $67 $69 $6E $09 $63 $3D $22 $74 $C3 $BA $22 $3B
Se puede apreciar, claramente, como las coordenadas lógicas son distintas a las coordenadas
físicas. Aquí podemos encontrar que la coordenada lógica X de la letra “c” es 7, pero su coordenada
física es 10.
Verticalmente la coordenada Y lógica y física son siempre iguales, así que no habrá que hacer
transformaciones.
El cursor siempre trabaja en coordenadas físicas. Las coordenadas del cursor se encuentran en
las propiedades CaretX, y CaretY.
Por ejemplo para posicionar el cursor en el quinto carácter de la segunda línea, haríamos:
SynEdit1.CaretX := 5;
SynEdit1.CaretY := 2;
También se puede hacer uso de la propiedad CaretXY, que es incluye las dos coordenadas X e Y
en una estructura de tipo TPoint:
4
Debido a la desigualdad en el tamaño de los caracteres en UTF-8, existen funciones específicas para el manejo de
cadenas en UTF-8, como Utf8Length, and Utf8Pos.
14 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Pos.y:=2;
SynEdit1.CaretXY := Pos; //Equivalente a SynEdit1.CaretXY := Point(5,2);
Se puede acceder también, a las coordenadas lógicas del cursor usando la propiedad:
SynEdit1.LogicalCaretXY. Así si lo que se tiene son coordenadas lógicas, para posicionar correctamente
el cursor en SynEdit, podemos hacer esto:
SynEdit1.LogicalCaretXY:=Point(5, 2);
Para realizar las transformaciones entre las coordenadas lógicas y físicas, existen un grupo de
funciones de transformación:
SynEdit1.LogicalToPhysicalCol();
SynEdit1.LogicalToPhysicalPos();
SynEdit1.PhysicalToLogicalCol();
SynEdit1.PhysicalToLogicalPos();
Consideremos un ejemplo en que tenemos contenido con vocales tildadas (se codifican con 2
bytes en UTF-8). Si en nuestro editor tenemos en la primera línea, el siguiente texto:
"ícono"
Y deseamos obtener el tercer carácter (que debe ser la letra “o” ). Para obtener la posición real
del carácter en la cadena debemos acceder a la posición:
SynEdit1.CaretX:=3;
SynEdit1.CaretY:=1;
xReal := SynEdit1.PhysicalToLogicalPos(SynEdit1.CaretXY).x
En xReal, obtendremos la posición real del carácter dentro de la cadena, que en nuestro caso es
4.
En ciertas ocasiones puede resultar útil conocer las coordenadas del cursor en pixeles. En este
caso se deben usar las propiedades:
SynEdit1.CaretXPix
SynEdit1.CaretYPix
La coordenada CaretXPix se mide desde el borde izquierdo del control (incluyendo el ancho del
Panel Vertical o Gutter) y no desde el área editable del editor.
15 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Como ya vimos, la posición del cursor se fija con las propiedades CaretX y CaretY, pero existen
ciertos límites con respecto a la ubicación del cursor.
En la práctica se puede decir que no hay límite práctico para el tamaño que puede tener una
línea en SynEdit, a menos que se quiera exceder de 2 mil millones. Pero el cursor tiene más
restricciones.
Un modo de trabajo extraño de SynEdit, es que puede ubicar al cursor más allá de los límites de
la línea. Por ejemplo, si la línea solo tiene 10 caracteres de ancho, el cursor podría ubicarse en la
posición 20:
Cursor
Flotante
Esta ubicación extraña del cursor se puede lograr tanto por teclado como por medio del ratón.
A este efecto le llamo “Cursor Flotante” y es una característica poco usual en los editores de
texto ya que lo normal es que no se pueda colocar el cursor más allá de los límites de la línea.
Este efecto solo es válido de forma horizontal ya que verticalmente el cursor estará siempre
limitado por la cantidad de líneas que existan en el texto, o dicho de otro modo, la coordenada Y del
cursor no puede ser mayor al número de líneas.
Para limitar la posición horizontal del cursor, dentro de los límites físicos de la línea actual, se
debe usar esta configuración:
Sin embargo, el modo de “cursor flotante”, puede limitarse por medio de la propiedad
“MaxLeftChar”. Si se fija un valor máximo para “MaxLeftChar”, no se permitirá ubicar horizontalmente
más allá de este valor prefijado, a menos que exista una línea que exceda este tamaño. Es decir, que
“MaxLeftChar” no limita el tamaño de la línea actual, pero puede limitar la posición horizontal del
cursor.
Para forzar a ubicar el cursor sin considerar el tamaño de la línea destino, se puede usar el
método MoveCaretIgnoreEOL(). Este método trabajará aun, cuando se exceda a “MaxLeftChar”.
16 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Otra peculiaridad, un tanto extraña de SynEdit, es que permite posicionar el cursor en medio de
una tabulación como si se tratara de simples espacios, dando la impresión de que no existe una
tabulación en esa posición.
Para notar la existencia de dicha tabulación, se puede intentar seleccionar un espacio dentro de
la zona de tabulación. Si la selección no es posible, indicará que se está dentro de una tabulación.
Si se desea deshabilitar este comportamiento y forzar a tratar las tabulaciones como un solo
carácter en el editor, se debe activar la opción “eoCaretSkipTab”, de la propiedad Options2:
La posición del cursor se puede cambiar a voluntad, usando las propiedades: CaretX, CaretY o
CaretXY, como ya se ha mencionado, pero también se puede posicionar el cursor por comandos (Ver
Sección 1.5.1 - Ejecutar comandos).
El siguiente código muestra como posicionar el cursor al final de todo el texto de SynEdit:
SynEdit1.ExecuteCommand(ecEditorBottom, '',nil);
Para desplazar el cursor como si se usaran las teclas direccionales, se deben usar los comandos
“ecLeft”, “ecRight”, “ecUp”, y “ecDown”.
Para cambiar el salto de línea (DOS, UNIX, MAC) que se usa con SaveToFile(), se debe usar la
unidad SynEditLines y ejecutar el método FileWriteLineEndType de la clase TSynEditLines:
17 de 153 15/01/2018
Lazarus La Biblia del SynEdit
18 de 153 15/01/2018
Lazarus La Biblia del SynEdit
El contenido del editor SynEdit, usualmente será modificado por el usuario, usando el teclado,
pero muchas veces requeriremos tomar el control del editor desde dentro del programa. Aquí
describiremos las diversas formas de acceder al contenido del editor.
Para borrar todo el contenido del editor se debe usar el método “ClearAll”:
SynEdit1.ClearAll;
SynEdit1.Text:='Hola mundo';
Escribir de esta forma, hará que se pierda toda la información previa que pudiera contener el
control (sin opción de deshacer), porque estamos asignándole un nuevo valor. Si solo quisiéramos
agregarle información podríamos hacer:
La propiedad Text, es una simple cadena y nos permite leer o escribir en el contenido del Editor.
Como se trata de un capo de tipo cadena, se pueden realizar con él, las mismas operaciones que se
hacen con las cadenas (búsqueda, concatenación, etc.).
SynEdit1.InsertTextAtCaret('texto insertado');
19 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Otra forma sería usando el método TextBetweenPointsEx(), que tienen más opciones para
controlar el cursor, después del reemplazo.
Prácticamente todo lo que se puede hacer con el teclado en el editor, se puede hacer también
con el uso de comandos. Por ejemplo, para insertar el caracter “x” en la posición actual del cursor, se
debe usar:
SynEdit1.ExecuteCommand(ecChar, 'x',nil);
Existe un grupo enorme de comandos que se pueden ingresar a SynEdit. Todos ellos están
declarados en la unidad “SynEditKeyCmds”. Se muestran algunos de ellos:
20 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Como se ven, los comandos pueden realizar todo tipo de acciones como pegar texto, borrar un
carácter o deshacer los cambios.
Las acciones que se pueden realizar, incluyen también el manejo de los marcadores y el plegado
de bloques de texto. Los siguientes comandos sirven para esta función:
21 de 153 15/01/2018
Lazarus La Biblia del SynEdit
EcFoldLevel6 = EcFoldLevel5 + 1;
EcFoldLevel7 = EcFoldLevel6 + 1;
EcFoldLevel8 = EcFoldLevel7 + 1;
EcFoldLevel9 = EcFoldLevel8 + 1;
EcFoldLevel0 = EcFoldLevel9 + 1;
EcFoldCurrent = 381;
EcUnFoldCurrent = 382;
EcToggleMarkupWord = 383;
El contenido del editor (las líneas de texto) se almacena en la propiedad “Lines”, que es una lista
de cadenas similar a un objeto “TStringList”, por lo tanto se puede acceder a el como a cualquier lista
común de cadenas. Por ejemplo para mostrar el contenido de la primera línea, haríamos:
showmessage(SynEdit1.Lines[0]);
Acceder a Lines[] es una forma rápida de acceso al contenido de SynEdit. Acceder de esta forma
nos permite un manejo directo de cadenas (porque se trata de una lista de cadenas), por lo que
podemos usar todas las funciones de cadena para nuestros propósitos. Empero, las modificaciones
realizadas no podrán ser canceladas con el método “Undo” del editor.
Como Lines[], contiene todo el texto de SynEdit, si se quisiera acceder a la primera línea de
SynEdit, debemos acceder a Lines[0]. Así para escribir un texto en la primera fila de SynEdit, debemos
hacer:
SynEdit1.Lines[0]:='Hola mundo';
Esta instrucción funcionará siempre, porque SynEdit contiene por lo menos una línea de texto,
pero si intentáramos acceder a Lines[1], sin que existe una segunda línea en el editor, se generará un
error en tiempo de ejecución.
Para conocer la cantidad de líneas del editor podemos usar el método Count:
NumLineas := SynEdit1.Lines.Count;
Como Lines[] es una lista, comparte muchos de los métodos de las listas que conocemos. Por
ejemplo para agregar una línea más al editor podemos hacer:
PROPIEDAD DESCRIPCIÓN
22 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Para iterar sobre todo el contenido de Lines[], se puede usar la siguiente construcción:
Para realizar modificaciones, es preferible hacerlas usando comandos del editor que accediendo
directamente a “Lines[]”, para mantener las opciones “Undo”, activas. Sin embargo, modificaciones
complejas por comandos, pueden ser mucho más lentas que las modificaciones a Lines[].
1.5.3 El Portapapeles
SynEdit1.CopyToClipboard;
SynEdit1.CutToClipboard;
SynEdit1.PasteFromClipboard;
No es necesario identificar que hace, porque sus nombres son bastante conocidos, y ya sabemos
que hacen un movimiento de datos entre el texto seleccionado y el portapapeles.
Las opciones del portapapeles también se pueden acceder mediante el uso de comandos:
23 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Como el portapapeles trabaja con la selección, es usual trabajar con las propiedades
“BlockBegin” y “BlockEnd”.
24 de 153 15/01/2018
Lazarus La Biblia del SynEdit
SynEdit tiene un control muy bueno de los cambios realizados en el texto. Permite deshacer y
rehacer los cambios en casi todos los casos. Estos cambios pueden ser inclusive modificaciones en modo
columna.
Cada vez que se hace un cambio, SynEdit, guarda el cambio hecho en una memoria interna
MÉTODO/PROPIEDAD DESCRIPCIÓN
Casi todos los cambios hechos manualmente desde el teclado en un SynEdit, se pueden
“deshacer”, sin embargo, cuando se cambia el contenido desde código, se debe tener en cuenta que
algunas acciones no podrán deshacerse.
La siguiente tabla muestra los métodos de cambio realizados en un SynEdit y si estos admiten
“deshacer”.
25 de 153 15/01/2018
Lazarus La Biblia del SynEdit
SynEdit1.PasteFromClipboard;
Hay que considerar los métodos que debemos usar para realizar los cambios en un SynEdit, si
queremos mantener las opciones de “deshacer”.
Usualmente los cambios que generen, cada instrucción que modifique el contenido de SynEdit y
que soporte “deshacer”, podrá ser desechada con una simple llamada a “Undo”.
Si se quisiera agrupar varias acciones para deshacerse con un solo “Undo”, se debe usar los
métodos BeginUndoBlock y EndUndoBlock:
SynEdit1.BeginUndoBlock;
//Aquí pueden haber varios cambios que soporten deshacer.
...
SynEdit1.EndUndoBlock;
Con esta construcción lograremos deshacer todos los cambios hechos con una sola llamada a
“Undo”.
Puede ser práctico, usar la construcción BeginUpdate y EndUpdate, para evitar que se refresque
el control hasta que se hayan terminado de realizar todos los cambios sobre SynEdit:
Trabajar de esta forma nos permite mejorar la velocidad de los cambios, porque no se debe
refrescar el control cada vez que se modifique algo.
Cuando esté deshabilitado el refresco de SynEdit, se ignorarán inclusive las peticiones de tipo
Application.ProcessMessages().
26 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Como la mayoría de editores actuales, SynEdit maneja solo un bloque de selección5. Este bloque
es el que se usa para las operaciones de cortado y copiado de texto, pero también se usa para modificar
un texto por sobre-escritura:
La selección se define, por código, usando las propiedades “BlockBegin” y “BlockEnd” que son
de tipo “Tpoint”. En el ejemplo anterior, los valores asignados a “BlockBegin” y “BlockEnd”, son:
SynEdit1.BlockBegin.x:=4;
SynEdit1.BlockBegin.y:=1;
SynEdit1.BlockEnd.x:=4;
SynEdit1.BlockEnd.y:=2;
Otra forma de definir el bloque de selección es usar las propiedades “SelStart” y “SelEnd”. Estas
propiedades permiten trabajar de forma equivalente a “BlockBegin” y “BlockEnd”, pero no son de tipo
“TPoint”, sino que son simples enteros que mapean el texto como si fueran una serie ininterrumpida de
caracteres.
Los saltos de línea se consideran como dos caracteres en Windows y pueden también ser parte
de la selección.
5
Existen editores que pueden manejar más de un bloque de selección, pero la enorme mayoría solo trabaja con un
solo bloque de selección.
27 de 153 15/01/2018
Lazarus La Biblia del SynEdit
El bloque de selección se define desde la posición del carácter apuntado por “SelStart”, hasta el
carácter anterior al carácter apuntado por “SelEnd”. Por lo tanto, la cantidad de caracteres
seleccionados será igual a (SelEnd-SelStart).
Cuando se desea seleccionar una región relativa a la posición actual del cursor, se pueden usar
estos métodos:
Todos estos métodos de selección se basan en la posición actual del cursor y funcionan como si
se hiciera la selección de forma manual, considerando que SelectWord identifica una palabra usando
solo los caracteres alfabéticos, incluyendo los caracteres tildados y la letra ñ.
EL parámetro opcional permite indicar si se quiere incluir los espacios en blanco inicial y final
como parte de la selección. Si se pone en FALSE, la selección de la línea actual podría no ser completa si
es que hubieran espacios iniciales o finales en la línea.
El método SelectToBrace, permite seleccionar bloques de texto que estén delimitados por
paréntesis, llaves o corchetes. Solo funcionará cuando se cumplan las siguientes condiciones:
• Que el carácter actual donde se encuentre el cursor sea; ‘(‘, ‘{‘ o ‘[’o que el carácter anterior
al que se encuentre el cursor sea ‘)’, ‘}’, o ‘]’.
• Que existe el delimitador correspondiente en la misma línea o en otra línea cualquiera.
Para determinar si hay selección activa (texto seleccionado), se debe usar la propiedad
“SelAvail”:
28 de 153 15/01/2018
Lazarus La Biblia del SynEdit
La propiedad “SelText”, es también de escritura, de modo que nos permite modificar el texto
seleccionado. El siguiente código elimina el texto seleccionado:
SynEdit1.SelText := '';
Sin embargo, para borrar la selección existe el método “ClearSelection”, que es una forma
abreviada.
SynEdit1.SelectAll;
SynEdit1.ClearSelection;
SynEdit1.ExecuteCommand(ecDeleteLastChar,'',nil);
SynEdit1.TextBetweenPoints[editor.BlockBegin,editor.BlockEnd] := '';
29 de 153 15/01/2018
Lazarus La Biblia del SynEdit
En este modo el área de selección forma un rectángulo e incluye solo parcialmente a las líneas
en su camino.
Una vez que se tiene el texto seleccionado se pueden ejecutar las acciones de cortado, copiado
o pegado. También se puede sobrescribir la selección pulsando cualquier tecla.
Hay que notar que cualquier tecla pulsada que no sea la combinación,
<Alt>+<Shift>+direccionales, hará que el modo de selección en modo columna termine.
Para pasar al modo columna por programa, se puede usar el siguiente código:
var pos:Tpoint;
...
pos.x:=3;
pos.y:=2;
SynEdit1.BlockBegin:= pos; //define punto inicial de selección
pos.x:=8;
pos.y:=3;
SynEdit1.BlockEnd:= pos; //define punto final de selección
SynEdit1.CaretXY := pos;
SynEdit1.SelectionMode:=smColumn; //cambia a modo columna
...
De igual forma, en este caso, cualquier tecla pulsada que no sea la combinación,
<Alt>+<Shift>+direccionales, hará que el modo de selección en modo columna termine.
• smNormal,
• smColumn
• smLine,
El modo smNormal, es el modo que está activo por defecto y es el modo de selección normal.
30 de 153 15/01/2018
Lazarus La Biblia del SynEdit
El modo smLine, es un modo de selección que hará que todas las líneas entre BlockBegin y
BlockEnd, se marquen como parte de la selección.
También se puede realizar selección por columnas usando los comandos de teclado:
Se pueden definir bloques de selección usando BlockBegin y BlockEnd. Por ejemplo si se tiene un
editor “ed” de tipo TSynEdit, se puede seleccionar con este código:
p: Tpoint;
...
ed.Text:='el quijote';
31 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Sin embargo, si el SynEdit trabajara en UTF-8 (como suele hacerlo), un carácter especial, puede
tener dos caracteres de ancho. Por ello debe tenerse cuidado con las coordenadas lógicas (las que
maneja BlockBegin y BlockEnd ) y las coordenadas físicas.
Si la cadena en el editor hubiera sido “él quijote”, la selección tendría otro resultado:
ed.Text:='él quijote';
//fija inicio de bloque
p:= ed.BlockBegin;
p.x:=1;
ed.BlockBegin := p;
32 de 153 15/01/2018
Lazarus La Biblia del SynEdit
33 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Si la selección solo tiene una fila, el punto BlockBegin apuntará siempre en la columna de la
izquierda de la selección, no importa el sentido desde donde se haya hecho la selección.
Si la selección tiene varias filas, el punto BlockBegin siempre aparecerá en la fila superior, no
importa el sentido desde donde se haya hecho la selección.
Si la selección solo tiene una fila, el punto BlockBegin apuntará siempre en la columna de la
izquierda de la selección, no importa el sentido desde donde se haya hecho la selección.
34 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Si la selección tiene varias filas, el punto BlockBegin siempre aparecerá en la fila superior, no
importa el sentido desde donde se haya hecho la selección.
En los siguientes ejemplos, se ha hecho una selección de izquierda a derecha, hacia abajo y
hacia arriba:
En los siguientes ejemplos, se ha hecho una selección de derecha a izquierda, hacia abajo y
hacia arriba:
En todos los casos vistos el cursor siempre tomará el valor de BlockBegin o de BlockEnd.
35 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Casi todo editor de texto, tiene incluido opciones para búsqueda y reemplazo. Así que es útil
saber cómo implementar las opciones de búsqueda y reemplazo en SynEdit.
Sin embargo, SynEdit, tiene ya incluidas, dos funciones para la búsqueda reemplazo:
Estas funciones nos proveen de las funcionalidades de búsqueda hacia arriba o abajo, con
sensibilidad de caja (mayúscula/minúscula), palabra completa, o el uso de expresiones regulares.
TSynSearchOption =
( ssoMatchCase,
ssoWholeWord,
ssoBackwards,
ssoEntireScope,
ssoSelectedOnly,
ssoReplace,
ssoReplaceAll,
ssoPrompt,
ssoSearchInReplacement, //continue search-replace in replacement
//(with ssoReplaceAll) replace recursive
ssoRegExpr,
ssoRegExprMultiLine,
ssoFindContinue // Assume the current selection is the last match,
36 de 153 15/01/2018
Lazarus La Biblia del SynEdit
1.7.1 Búsqueda
var
encon : integer;
buscado : string;
begin
buscado := 'texto a buscar';
encon := editor.SearchReplace(buscado,'',[]);
if encon = 0 then
ShowMessage('No se encuentra: ' + buscado);
...
El hecho de seleccionar el texto encontrado ocasiona que el cursor se traslade al final del texto
seleccionado. De modo que la siguiente llamada a SearchReplace(), buscará a partir de esta posición
(buscar siguiente).
encon := editor.SearchReplace(buscado,'',[ssoBackwards]);
37 de 153 15/01/2018
Lazarus La Biblia del SynEdit
encon := ed.SearchReplace(buscado,'',[ssoMatchCase]);
Cuando se indica la opción “ssoEntireScope”, la búsqueda se hace siempre desde el principio del
texto (independientemente de donde se encuentre el cursor) y se detiene al encontrar el primer
elemento.
Existe entre los controles de Lazarus, un diálogo creado específicamente para operaciones de
búsqueda. Este componente es FIndDialogo, y se encuentra en la paleta de componentes, en la pestaña
“Dialogs”.
encon := editor.SearchReplace(buscado,'',opciones);
38 de 153 15/01/2018
Lazarus La Biblia del SynEdit
if encon = 0 then
ShowMessage('No se encuentra: ' + buscado);
end;
Ahora desde alguna parte estratégica de nuestro programa (como la respuesta al menú),
debemos incluir el código para abrir el diálogo y poder iniciar la búsqueda:
Al pulsar sobre “Find”, se llamará al evento OnFind, que debe estar asociado al método
“FindDialog1Find” que hemos creado. El diálogo, sin embargo permanecerá visible, hasta pulsar en
“Cancel” o pulsar la tecla <escape>.
Si esta ventana no se adapta a las necesidades de búsqueda, requeridas se puede crear siempre
un formulario especial para nuestra búsqueda personalizada.
1.7.3 Reemplazo
El proceso de reemplazo es similar al de búsqueda. Para buscar una cadena simple, se puede
usar el siguiente código:
var
encon : integer;
buscado : string;
begin
buscado := FindDialog1.FindText;
39 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Este modo de funcionamiento es útil en modo de búsqueda, pero en modo de reemplazo puede
resultar extraño, ya que no se pide ninguna confirmación para remplazar el texto y el reemplazo se
produce instantáneamente, sin ninguna selección previa.
Para mejorar este comportamiento, se puede agregar una ventana de confirmación, antes del
reemplazo, de modo que sirva, a la vez, para ver el texto que va a ser reemplazado.
No hay un diálogo predefinido para crear una ventana de confirmación. Si queremos usar una
tendremos que crearlo nosotros mismos.
Un diálogo sencillo que nos podría servir sería un MessageBox() con los botones Si-No-
Cancelar. De modo que nuestro procedimiento de reemplazo, podría escribirse de la siguiente forma:
var
encon, r: integer;
buscado : string;
opciones: TSynSearchOptions;
ed : TSynEdit;
begin
...
buscado := 'cadena buscada';
opciones := []; //opciones de búsqueda
encon := ed.SearchReplace(buscado,'',opciones); //búsqueda
while encon <> 0 do begin
//pregunta
r := Application.MessageBox('¿Reemplazar esta
ocurrencia?','Reemplazo',MB_YESNOCANCEL);
if r = IDCANCEL then exit;
if r = IDYES then begin
ed.TextBetweenPoints[ed.BlockBegin,ed.BlockEnd] := 'nueva cadena';
end;
40 de 153 15/01/2018
Lazarus La Biblia del SynEdit
//busca siguiente
encon := ed.SearchReplace(buscado,'',opciones); //búsca siguiente
end;
ShowMessage('No se encuentra: ' + buscado);
end.
La idea aquí es preguntar antes de cada reemplazo, daño la opción de omitir alguna ocurrencia o
cancelar todo el proceso.
En este modo de trabajo, no estamos haciendo uso de la opción “ssoReplace”, sino que
usamos SearchReplace() , únicamente en modo de búsqueda. El reemplazo lo hacemos en el editor
usando el método “TextBetweenPoints()”.
Así como para la búsqueda existe el diálogo TFindDialog, también es posible usar el diálogo
“TReplaceDialog” desde la paleta de componentes.
Este diálogo nos facilita la entrada de datos para iniciar una Búsqueda/Reemplazo, pero
tampoco nos proporciona un diálogo de confirmación para antes de remplazar.
El modo de trabajo con este diálogo es sencillo. Se deben asignar los eventos “OnFind” y
“OnReplace”. El primer evento se ejecutará cuando se pulse el botón “Find more” y el segundo se
ejecutará con el botón “Replace” o con el botón “Replace all”.
41 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Este código corresponde al que debe asociarse al evento “OnReplace”. El código para el evento
“OnFind”, puede ser una simple búsqueda:
encon := ed.SearchReplace(buscado,'',opciones);
if encon = 0 then
ShowMessage('No se encuentra: ' + buscado);
end;
42 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Este diálogo, tiene diversas opciones que pueden ser personalizadas desde la propiedad
“Options”. Estas opciones permiten ocultar o mostrar ciertos botones.
43 de 153 15/01/2018
Lazarus La Biblia del SynEdit
SynEdit es un componente bastante completo. Entre sus diversas opciones, incluye remarcado
del contenido. Estas opciones están incluidas en el mismo código del editor y son independientes del
uso de los resaltadores de sintaxis, descritos en la sección 2.
Hay que indicar que el texto a resaltar, puede ser cualquier combinación de caracteres, no
necesariamente letras.
Para especificar que solo se marquen palabras completas, se puede usar la opción
“ssoWholeWord”:
SynEdit1.HighlightAllColor.Background := clGreen;
SynEdit1.SetHighlightSearch('Mundo', [ssoSelectedOnly,ssoWholeWord]);
TSynSearchOption =
( ssoMatchCase, ssoWholeWord,
ssoBackwards,
ssoEntireScope, ssoSelectedOnly,
ssoReplace, ssoReplaceAll,
ssoPrompt,
ssoSearchInReplacement,
ssoRegExpr, ssoRegExprMultiLine,
ssoFindContinue
44 de 153 15/01/2018
Lazarus La Biblia del SynEdit
);
Como se ve, son las mismas opciones que se usan para Búsqueda/Reemplazo. Solo algunas de
estas opciones funcionan con SetHighlightSearch().
Este tipo de remarcado, utiliza las mismas opciones que cuando se realiza una búsqueda de
texto (Ver Sección 1.7 - Búsqueda y Reemplazo), así que tiene las mismas consideraciones.
El texto puede modificarse, pero cada vez que se encuentre la secuencia buscada, se marcará
nuevamente el texto. Es como tener implementado un buscador permanente de texto.
Este método de remarcado de texto, funciona como un resaltador sencillo de sintaxis, pero es
limitado porque solo se aplica a un solo texto y porque solo reconoce identificadores y símbolos.
Existe otra forma de remarcado de texto que es aplicable solamente a palabras y en la posición
actual del cursor. Una palabra solo puede contener caracteres alfanuméricos, el signo dólar y el guion
bajo. También se incluyen las vocales acentuadas y la letra ñ.
Este código hará que se marquen las palabras iguales a la palabra en la que está el cursor, tal
como se muestra en la siguiente figura:
45 de 153 15/01/2018
Lazarus La Biblia del SynEdit
El editor auotmáticamente reconoce la palabra que está debajo del cursor y toma esta para
realizar el marcado en todo el texto. Por defecto se ignora el tipo de caja (mayúscula o minúscula).
El remarcado, puede incluir diversos atributos como color de fondo, color de texto, o borde.
Estos atributos se acceden mediante la propiedad “MarkupInfo”.
Esta opción de remarcado, funciona también con el texto seleccionado. De forma que se buscará
solo el texto que se encuentre dentro de la selección:
Hay que notar que el remarcado no lee todo el contendio del editor completo. Por defecto solo
leerá hasta 100 líneas adelante y atrás de la pantalla actual.
Muchas veces resulta conveniente marcar la línea actual para identificar fácilmente, dónde se
encuentra el cursor. SynEdit permite cambiar fácilmente el color de fondo de la línea en donde se
encuentra el cursor:
46 de 153 15/01/2018
Lazarus La Biblia del SynEdit
SynEdit1.LineHighlightColor.Background:=clYellow;
La instrucción anterior pintará de amarillo, el fondo de la línea actual. El resultado será similar al
mostrado en la siguiente figura:
Para desactivar el resaltado de la línea actual, se puede usar el mismo color de fondo del editor,
o se puede usar el color clNone:
SynEdit1.LineHighlightColor.Background:=clNone;
Aquí se puede notar que existen diversas propiedades adicionales a “Background”, para cambiar
la apariencia que tendrá la línea resaltada. Todas ellas se pueden configurar por código, o desde el
inspector de objetos.
47 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Es posible indicar a SynEdit que remarque una o varias líneas del contenido. Para ello primero
debemos crear un método en nuestro formulario que identifique la línea a marcar y asigne los atributos:
SynEdit1.OnSpecialLineMarkup := @SynEdit1SpecialLineMarkup;
Cada vez que el editor explora una línea, llama al evento “OnSpecialLineMarkup”, para ver si tiene
algún atributo especial o es una línea común. La implementación de este evento, debe ser de respuesta
rápida para evitar interferir en el funcionamiento de SynEdit.
En este caso hemos activado el resaltado de la segunda línea, y permanecerá así marcada hasta
que anulemos el evento. La línea marcada será siempre la segunda, aun cuando se inserten o eliminen
líneas. Para marcar una línea que siga al texto contenido, se debe agregar un procesamiento adicional.
Si se cambia la línea remarcada por código, se debe refrescar al editor para que actualice la nueva
línea marcada. Esto se puede hacer llamando al método “SynEdit1.Refresh” pero se recomienda usar
método “SynEdit1.Invalidate”, que es más eficiente.
Existe además, el evento OnSpecialLineColors() que permite realizar una tarea similar, pero solo
permite cambiar el color de fondo y el color del texto. Este evento está definido como:
48 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Los marcadores de texto son ubicaciones especiales fijadas en el contenido de SynEdit. Podría
decirse que son el equivalente digital de la cinta (separador) que sirve para marcar una página en un
libro.
Sin embargo los marcadores de SynEdit, guardan posiciones que incluyen fila y columna, y puede
haber tantos como se quiera. Los marcadores de texto se pueden mostrar gráficamente como un ícono
en el Panel Vertical:
Los marcadores de texto, a diferencia de los vistos anteriormente, no resaltan el contenido del
editor, sino que almacenan una ubicación y tienen la opción de mostrar un ícono en el panel vertical del
editor.
Estos marcadores son útiles para guardar las posiciones de ciertas líneas de texto. Si se insertan o
se eliminan líneas, el marcador tratará de mantener su ubicación, siguiendo a su línea.
Para crear un marcador, se debe incluir la unidad SynEditMarks y se debe tener una lista de
íconos, almacenados en un TImageList. El siguiente código, usado para la figura anterior, crea un
marcador y lo hace visible en la línea 2:
var
marca: TSynEditMark;
...
49 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Es importante que la línea donde se ubique el marcador sea válida, o de otra forma no aparecerá
de ningún modo.
Los marcadores mostrados, se manejan simplemente como posiciones especiales en el texto, sin
ningún orden en especial.
Existen, sin embargo, otro tipo de marcadores llamados “BookMark”, que ofrecen además un
número que los puede identificar.
Los “BookMark” se suelen crear de forma distinta. Igualmente se requiere una lista de imágenes:
SynEdit1.BookMarkOptions.BookmarkImages := ImageList1;
SynEdit1.SetBookMark(0, 1, 1); //BookMark 0 en la línea 1
SynEdit1.SetBookMark(1, 1, 3); //BookMark 1 en la línea 3
Aunque los íconos usados para los “BookMark”, pueden ser de cualquier tipo, lo usual es usar
íconos que se asocien al número del marcador:
SynEdit1.ClearBookMark(5);
Para acceder a un marcador cualquiera (puede ser “BookMark” o no), se hace como si fuera un
elemento de una matriz:
marca := SynEdit1.Marks[0];
if marca.IsBookmark then ...
50 de 153 15/01/2018
Lazarus La Biblia del SynEdit
var
marca: TSynEditMark;
...
SynEdit1.BookMarkOptions.BookmarkImages := ImageList1;
marca := TSynEditMark.Create(SynEdit1) ;
marca.Line := 1;
marca.Column := 5;
marca.ImageIndex := 0;
marca.BookmarkNumber := 1;
marca.Visible := true;
marca.InternalImage := false;
SynEdit1.Marks.Add(marca);
SynEdit1.BookMarkOptions.BookmarkImages := ImageList1;
SynEdit1.ExecuteCommand(ecSetMarker4,#0,nil);
Por defecto, se crean atajos para agregar marcadores (BookMark), en SynEdit, al momento de
agregar el control. Estos atajos son del tipo <Shift>+<Ctrl>+<número de marcador>.
Si se intentara agregar un marcador, sin tener una lista de íconos asignada, se generará un error
en tiempo de ejecución.
Para deshabilitar los marcadores, se puede ocultar la parte correspondiente a los marcadores, en
el panel vertical (Gutter), o se puede eliminar también los atajos de teclado (Propiedad “Keystrokes”).
Los marcadores más básicos en SynEdit, son objetos de la clase “TsynEditMarkup”. Todos los
marcadores derivan directa o indirectamente de “TsynEditMarkup”.
51 de 153 15/01/2018
Lazarus La Biblia del SynEdit
TSynEditMarkup = class(TObject)
private
...
protected
...
public
constructor Create(ASynEdit : TSynEditBase);
destructor Destroy; override;
Procedure PrepareMarkupForRow(aRow : Integer); virtual;
Procedure FinishMarkupForRow(aRow : Integer); virtual;
Procedure EndMarkup; virtual;
Function GetMarkupAttributeAtRowCol(const aRow: Integer;
const aStartCol: TLazSynDisplayTokenBound;
const AnRtlInfo: TLazSynDisplayRtlInfo) : TSynSelectedColor; virtual;
abstract;
Procedure GetNextMarkupColAfterRowCol(const aRow: Integer;
const aStartCol: TLazSynDisplayTokenBound;
const AnRtlInfo: TLazSynDisplayRtlInfo;
out ANextPhys, ANextLog: Integer); virtual; abstract;
procedure MergeMarkupAttributeAtRowCol(const aRow: Integer;
const aStartCol, AEndCol :TLazSynDisplayTokenBound;
const AnRtlInfo: TLazSynDisplayRtlInfo;
AMarkup: TSynSelectedColorMergeResult); virtual;
52 de 153 15/01/2018
Lazarus La Biblia del SynEdit
end;
Para crear un marcador nuevo debemos crear un objeto de tipo “TsynEditMarkup” o de alguno de
sus descendientes.
Ya hemos visto cómo crear un marcador en la sección 1.8.2 - Remarcado de la palabra actual,
cuando creamos un objeto de la clase “TSynEditMarkupHighlightAllCaret”, porque esta clase deriva de
“TsynEditMarkup”. También la clase “TSynEditMarkupHighlightAll”, usada para marcar una palabra
cualquiera, es una descendiente de “TsynEditMarkup”.
El componente SynEdit, viene con varios marcadores, definidos por defecto. Una exploración de
ellos se logra iterando sobre la tabla Markup[]:
La mayoría de marcadores predefinidos en SynEdit, tienen su campo “StoredName” vacío, así que
el lazo anterior mostrará cadenas nulas, pero si hubiéramos creado un marcador con nombre,
podríamos ubicarlo de esta forma.
var
marc: TSynEditMarkup;
...
marc := nil;
//busca marcador por nombre
for i:= 0 to ed.MarkupCount-1 do begin
tmp := ed.Markup[i].MarkupInfo.StoredName;
if ed.Markup[i].MarkupInfo.StoredName='ResPalabraActual' then
marc := ed.Markup[i];
end;
if marc<>nil then begin //hay marcador
marc.Enabled:=false; //desactiva
end;
Otra forma de ubicar un marcador en SynEdit es usar el método MarkupByClass[], que permite
especificar el nombre de la clase del marcador que queremos encontrar:
marc := SynEdit1.MarkupByClass[TSynEditMarkupHighlightAllCaret];
53 de 153 15/01/2018
Lazarus La Biblia del SynEdit
El siguiente código, muestra cómo definir el color del borde del texto remarcado, por el marcador
TSynEditMarkupHighlightAllCaret:
var
marc: TSynEditMarkup;
...
marc := SynEdit1.MarkupByClass[TSynEditMarkupHighlightAllCaret];
TSynEditMarkupHighlightAllCaret(marc).MarkupInfo.FrameColor := clGreen;
Para acceder a las propiedades visuales del texto remarcado, se usa la propiedad “MarkupInfo”
del resaltador.
Los marcadores vistos en las secciones anteriores, que resaltan la apariencia del texto mostrado,
son relativamente simples (remarcado de una palabra o de una línea). Sin embargo, los marcadores de
SynEdit son bastante elaborados, y permiten implementar casos de marcas complejas del texto.
Un ejemplo de marcadores más completos, son los que usa el editor de Lazarus, que permiten
resaltar el inicio y fin de bloques de código como BEGIN-END o REPEAT-UNTIL.
El siguiente ejemplo, que supone un formulario, con un control SynEdit en él, muestra cómo crear
un marcador simple, que resalta un bloque de texto, en la primera línea:
unit Unit1;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, SynEdit,
SynEditTypes, SynEditMarkupHighAll;
type
TMarkup = class(TSynEditMarkupHighlightMatches);
{ TForm1 }
TForm1 = class(TForm)
SynEdit1: TSynEdit;
procedure FormCreate(Sender: TObject);
private
Markup: TMarkup; //marcador 1
54 de 153 15/01/2018
Lazarus La Biblia del SynEdit
end;
var
Form1: TForm1;
implementation
{$R *.lfm}
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
Markup := TMarkup.Create(SynEdit1);
Markup.MarkupInfo.FrameColor := clRed;
Markup.MarkupInfo.FrameEdges := sfeBottom;
SynEdit1.MarkupManager.AddMarkUp(Markup); //agrega marcador
Markup.Matches.StartPoint[0] := Point(2,1);
Markup.Matches.EndPoint[0] := Point(4,1);
Markup.InvalidateSynLines(1,1);
end;
end.
El bloque a resaltar, se define con coordenadas de tipo TPoint, en StartPoint[] y EndPoint[]. Notar
que por defecto, el marcador está habilitado.
Para crear marcadores más elaborados, se debe acceder a campos protegidos de SynEdit, así que
se debe crear una clase que derive de SynEdit y crear nuestro editor a partir de esa clase. Una vez
definido nuestro editor, tendremos la posibilidad de crear cualquier marcador, cuya lógica debe ser
definida por código.
55 de 153 15/01/2018
Lazarus La Biblia del SynEdit
El siguiente código crea un editor a partir de SynEdit, creando una clase descendiente de SynEdit
(subclass), y define dos bloques de texto para remarcarlos:
interface
uses
Classes, SysUtils, Forms, Controls, Graphics,
SynEdit, SynEditMarkupSelection,
SynEditTypes, SynEditPointClasses, SynEditMarkup;
type
{ TMiEditor }
TMiEditor = class(TSynEdit) //define la clase de mi Editor
private
Bloque1: TSynEditSelection; //bloque de selección 1
Markup1: TSynEditMarkupSelection; //marcador 1
{ TForm1 }
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
MiEditor: TMiEditor;
end;
var
Form1: TForm1;
implementation
{ TMiEditor }
constructor TMiEditor.Create(AOwner: TComponent);
var
MarkupManager: TSynEditMarkupManager;
begin
inherited Create(AOwner);
MarkupManager := TSynEditMarkupManager(MarkupMgr); //MarkupMgr es
"TObject"
//crea un bloque para remarcado
Bloque1 := TSynEditSelection.Create(ViewedTextBuffer, false);
Bloque1.InvalidateLinesMethod := @InvalidateLines;
Markup1 := TSynEditMarkupSelection.Create(self, Bloque1);
MarkupManager.AddMarkUp(Markup1); //agrega marcador
56 de 153 15/01/2018
Lazarus La Biblia del SynEdit
destructor TMiEditor.Destroy;
begin
Bloque1.Free; //Markup1, se destruye con SynEdit
Bloque2.Free; //Markup2, se destruye con SynEdit
inherited Destroy;
end;
{$R *.lfm}
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
MiEditor := TMiEditor.Create(self); //crea mi componente Editor
MiEditor.Parent:= self; //lo ubica en el formualrio
MiEditor.Align := alClient; //lo alinea
//escribe un texto
MiEditor.Lines.Add('En un lugar de la mancha');
MiEditor.Lines.Add('de cuyo nombre');
MiEditor.Lines.Add('No quiero acordarme');
//define sección 1 a remarcar
MiEditor.Bloque1.StartLineBytePos := Point(5,1);
MiEditor.Bloque1.EndLineBytePos := Point(8,1);
MiEditor.Markup1.Enabled := True;
MiEditor.Markup1.MarkupInfo.FrameColor := clRed;
57 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Una observación en este remarcado, es que las áreas resaltadas tienen coordenadas fijas, aunque
se modifique el texto. Si se desea que el remarcado, siga al texto, se debe implementar por código, la
lógica necesaria.
58 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Para iniciarla, se debe seleccionar un bloque de texto y pulsar la combianción Ctrl-J, que por
defecto, es la que inicia la edición síncrona. En ese momento cambiará de color la selección y se
marcarán los identificadores que sean igual al que está donde se encuentra el cursor. Si en ese estado,
se edita el identificador actual, cambiarán todos los demás identificadores similares que se encuentren
en la selección.
59 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Otra forma sería hacerlo por código, incluyendo la unidad SynPluginSyncroEdit y creando un
objeto TSynPluginSyncroEdit. También, en este caso, se debe fijar su propiedad “Editor”, al SynEdit al
que se le desea agregar la funcionalidad de edición síncrona.
SynEdit puede manejar la edición a través de múltiples cursores, de la misma forma a como si
hubiera uno solo.
El siguiente código configura un SynEdit, para la edición con múltiples, cursores, agregando la
combinación Ctrl+Shift+Click, para posicionar un cursor nuevo:
fMultiCaret := TSynPluginMultiCaret.Create(self);
with fMultiCaret do begin
Editor := SynEdit1;
with KeyStrokes do begin
Add.Command := ecPluginMultiCaretSetCaret;
Add.Key := VK_INSERT;
Add.Shift := [ssShift, ssCtrl];
Add.ShiftMask := [ssShift,ssCtrl,ssAlt];
end;
end;
Luego cuando se mantengan pulsadas las teclas Ctrl+Shift y se haga pulse el ratón en cualquier
parte del texto, se ubicará allí un nuevo cursor habilitado para la edición.
Cuando se tienen varios cursores, la acción que se haga se aplicará a todos los cursores creados,
incluyendo la inserción y borrado de caracteres.
60 de 153 15/01/2018
Lazarus La Biblia del SynEdit
PROPIEDAD DESCRIPCIÓN
BeginUpdate, EndUpdate Deshabilita y Habilita (respectivamente) el refresco de pantalla de
SynEdit. Ver 1.5.4 - Hacer y Deshacer.
BeginUndoBlock Permiten agrupar varias acciones de cambio, para deshacerse con una
EndUndoBlock sola llamada a Undo. Ver 1.5.4 - Hacer y Deshacer.
BlockBegin Indican las coordenadas del texto seleccionado. Ver Sección 1.6 - Manejo
BlockEnd de la selección
BlockIndent Indica la cantidad de espacios que se usan para indentar un bloque de
texto, cuando se ejecutan las acciones de indentación de bloques (En la
IDE de Lazarus están mapeadas como Ctrl+U y Ctrl+I).
BookMarkOptions Es un conjunto de opciones que permiten configurar los marcadores de
texto.
BracketHighlightStyle Configura el comportamiento del resaltado de los delimitadores
paréntesis, llaves y corchetes. Indica cómo se determina el resaltado.
Pueden ser:
sbhsBoth
sbhsLeftOfCursor
sbhsRightOfCursor
BracketMatchColor Atributo usado para resaltar los delimitadores paréntesis, llaves y
corchetes.
CanUndo Indica si hay acciones por deshacer en el editor
CanRedo Indica si hay acciones por rehacer en el editor
CanPaste Indica si hay contenido para pegar en el editor
CaretX Permiten leer o fijar la coordenada horizontal del cursor del editor. El
primer carácter tiene coordenada 1.
CaretY Permiten leer o fijar la coordenada vertical del cursor del editor. La
primera fila tiene coordenada 1.
CaretXY Permiten leer o fijar las coordenadas X, Y del cursor del editor. Ver
Sección 1.4.1 - Coordenadas del editor
ClearAll Borra todo el contenido del editor.
ClearSelection Borra el texto seleccionado.
ClearUndo Limpia y reinicia la memoria de “deshacer”. Una vez ejecutado, ya no se
podrán deshacer los cambios.
Color Color de fondo del editor.
CopyToClipboard Copia el texto seleccionado al portapapeles. Ver Sección 1.5.3 - El
Portapapeles.
CutToClipboard Corta el texto seleccionado al portapapeles. Ver Sección 1.5.3 - El
Portapapeles.
ExecuteCommand Envía un comando al editor. Ver sección 1.5.1 - Ejecutar comandos
ExtraCharSpacing Indica el espaciado que hay entre las letras. Por defecto es cero. Ver
Sección 1.3.3 - Tipografía.
ExtraLineSpacing Indica el espaciado que hay entre líneas. Por defecto es cero. Ver Sección
61 de 153 15/01/2018
Lazarus La Biblia del SynEdit
1.3.3 - Tipografía.
FoldAll Permite cerrar todos los bloques de plegado que existan en un editor.
También puede plegar por niveles.
Font Objeto que define las propiedades de la tipografía a usar en el editor.
Tiene diversas propiedades, como el nombre de la fuente, el tamaño, el
juego de caracteres, etc. Ver Sección 1.3.3 - Tipografía
Gutter Referencia al objeto que define el panel lateral del editor, donde suele
parecer el número de línea.
GetHighlighterAttriAtRowCol Lee el token y el atributo para una posición específica del texto. Solo es
válido cuando se tiene un resaltador asociado al editor.
Highlighter Referencia al resaltador que se va a usar para implementar el resaltado
de sintaxis (Ver Sección 2.3 - Coloreado de Sintaxis Usando Código).
InsertMode Permite pasar del modo normal al modo INSERT, en donde se sobre-
escriben los caracteres ingresados. Cuando se pone a FALSE, se entra a
modo de inserción.
InsertTextAtCaret Inserta un texto en la posición actual del cursor. Ver Sección 1.5 -
Modificar el contenido
Keystrokes Almacena los atajos de teclado, y los comandos a los cuales están
asociados esos atajos de teclado.
LogicalCaretXY Devuelve la posición del cursor en coordenadas lógicas. Medido en bytes
(no en caracteres).
LineHighlightColor Es el color de fondo de la línea en la que se encuentra el cursor en ese
momento. Ver sección 1.8.3 - Remarcado de la línea actual
LinesInWindow Indica la cantidad de Líneas y columnas visibles en pantalla. Depende
CharsInWindow únicamente del tamaño de la pantalla y el espaciado entre caracteres y
líneas.
Lines Lista de todo el contenido del editor. Es una lista de cadenas (similar a
TStringList), cada ítem representa una línea. Empieza en el elemento 0.
LineText Almacena siempre el contenido de la línea actual.
MaxLeftChar Limita la posición horizontal del cursor cuando se encuentra en modo
“Cursor flotante” - Ver sección 1.4.2 - Manejo del cursor
MaxUndo Número máximo de operaciones a deshacer.
Modified Indica cuando el contenido del editor ha sido modificado. También
puede escribirse.
MoveCaretIgnoreEOL Posiciona el cursor sin considerar los límites de la línea destino.
Options Diversas opciones adicionales para configurar al editor, como el
indentado automático, el comportamiento del cursor fuera de los límites
de la línea, o la conversión de las tabulaciones en espacios. Ver sección
1.10.1 - Propiedad Options
PasteFromClipboard Pega el texto del portapapeles en la posición de cursor. Ver Sección 1.5.3
- El Portapapeles.
Redo Vuelve a realizar una acción desecha con “Undo”. Ver Sección 1.5.4 -
Hacer y Deshacer.
RightEdge Indica la posición de la línea vertical (margen derecho) que se usa para
marcar el límite del área de impresión. Ver sección 1.3 - Apariencia
62 de 153 15/01/2018
Lazarus La Biblia del SynEdit
63 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Esta propiedad es un conjunto de tipo “TSynEditorOptions” que puede incluir los siguientes
elementos:
VALOR DESCRIPCIÓN
eoAutoIndent Posiciona el cursor en la nueva línea con la misma cantidad de
espacios que la línea anterior.
eoBracketHighlight Resaltará los delimitadores paréntesis, llaves y corchetes
(brackets).
eoEnhanceHomeKey Tecla “Home” saltará al inicio de la línea si está más cercana
(similar a Visual Studio)
eoGroupUndo Agrupa todos los cambios del mismo tipo, en una sola acción de
Deshacer/Rehacer, en lugar de manejar cada comando
separadamente.
eoHalfPageScroll Los desplazamientos página-arriba o página-abajo, solo saltarán
media página a la vez.
eoHideRightMargin Oculta la línea del margen derecho.
eoKeepCaretX Limita la posición del cursor para moverse, solo hasta el final de
la línea. De otra forma puede pasar hasta más allá de la línea.
Ver Sección 1.4.2 - Manejo del cursor
eoNoCaret Hace el cursor invisible.
eoNoSelection Deshabilita la selección del texto.
eoPersistentCaret No oculta el cursor cuando se pierde el foco.
eoScrollByOneLess Los desplazamientos página-arriba o página-abajo, serán con
una línea menos.
eoScrollPastEof Permiten al cursor pasar más allá de la marcar de fin de archivo.
eoScrollPastEol Permite al cursor pasar más allá del último carácter de una
línea. Funciona aun cuando eoKeepCaretX está presente.
eoScrollHintFollows El desplazamiento de la etiqueta (Hint) sigue al desplazamiento
del mouse al moverse verticalmente.
eoShowScrollHint Muestra una etiqueta (Hint) con el número de la primera línea
visible, al hacer desplazamiento vertical.
eoShowSpecialChars Muestra caracteres especiales.
eoSmartTabs Al tabular el cursor se ubicará en la posición del siguiente
espacio blanco de la línea previa.
eoTabIndent Las teclas <Tab> y <Shift><Tab> funcionarán para indentar o
quitar la indentación de un bloque seleccionado.
eoTabsToSpaces Hace que la tecla <Tab> inserte espacios (especificado en la
propiedad “TabWidth”) en lugar del carácter TAB. No convierte
64 de 153 15/01/2018
Lazarus La Biblia del SynEdit
VALOR DESCRIPCIÓN
eoCaretSkipsSelection El cursor saltará sobre la selección al usar las direccionales
derecha e izquierda.
eoCaretSkipTab El cursor saltará todos los espacios que componen una
tabulación. Sin esta opción, el cursor pasar por la tabulación
como si fueran espacios.
eoAlwaysVisibleCaret Mueve el cursor de modo que siempre sea visible al hacer
desplazamientos de pantalla.
eoEnhanceEndKey Al pulsar la tecla <End>, mueve al final de la línea, pero sin
considerar los espacios finales.
eoFoldedCopyPaste Mantiene las propiedades de plegado en operaciones de
copiado/pegado.
eoPersistentBlock Mantiene los bloques de selección, aun cuando el cursor esté
fuera del bloque.
eoOverwriteBlock No mantiene los bloques persistentes. Los sobre-escribe en
operaciones Insert/Delete.
eoAutoHideCursor Permite ocultar el cursor con operaciones del teclado.
65 de 153 15/01/2018
Lazarus La Biblia del SynEdit
2.1 Introducción
El resaltado de sintaxis es la capacidad de poder dar atributos visuales distintos a cada elemento
de un texto, de forma que lo haga más legible. Estos atributos solo se muestran en el editor más no
forman parte del texto, cuando se guardan en un archivo.
La siguiente figura muestra un trozo de código en Pascal, mostrado en un editor simple y en otro
con resaltado de sintaxis:
La diferencia es evidente. Un código con resaltado de sintaxis, es más fácil de entender y leer.
Sin embargo, este resaltado, debe hacerse de manera consistente y sencilla, de otra forma podría
confundir, en lugar de ayudar.
El resaltado de sintaxis, es también llamado “Coloreado de sintaxis”, por el uso común del color
como forma de diferenciar los elementos del texto, pero el color es solo uno de los atributos que
pueden ser usados, en un contexto más general, que puede incluir por ejemplo el tipo de letra. Para
generalizar el uso de atributos diversos, es que usamos el término “Resaltado de Sintaxis”, pero también
usaremos “Coloreado de Sintaxis”, como un sinónimo.
También podemos crear nuestro propio resaltador, si es que no encontramos uno que se adapte
a nuestras necesidades.
66 de 153 15/01/2018
Lazarus La Biblia del SynEdit
En la Sección 1.4.1 - Coordenadas del editor, vimos como SynEdit, interpreta los bytes de datos
para llegar al nivel de celdas de pantalla. Y que por lo general un carácter ocupará una celda.
Para implementar el resaltado de sintaxis, es necesario subir el nivel de análisis. Los caracteres
no son apropiados para determinar el resaltado (a menos que se quiera un resaltado carácter por
caracter), es mejor usar el concepto de “token”.
• Carácter.- Son los componentes visuales mínimos que componen un texto. Obedecen a una
codificación definida (UTF-8 en el caso de SynEdit). Permiten definir elementos de mayor
tamaño como los tokens. Por lo general ocupan una celda en la pantalla, pero podrían
ocupar dos.
• Atributo.- Son las propiedades de resaltado que se aplican a un token particular. En SynEdit,
un atributo está representado por un objeto de la clase “TSynHighlighterAttributes” y
permiten definir propiedades como el color del texto, el color del fondo, el tipo de letra, la
activación de negrita, y otras propiedades más.
El resaltado de sintaxis empieza en el nivel de los tokens y cada token puede tener atributos
diferentes del resto de tokens. Se puede decir que la unidad mínima de información para los
resaltadores de sintaxis en SynEdit es el token.
bytes 7B 78 3D 31 32 3B 63 61 64 3D 22 74 C3 BA 22 7D 3B 32 32 2F 2F 63 6F 6D 65 6E 74
caracteres { x = 1 2 ; c a d = " t ú " } ; / / c o m e n t
celdas { x = 1 2 ; c a d = " t ú " } ; / / c o m e n t
identificador
identificador
comentario
número
simbolo
simbolo
simbolo
simbolo
simbolo
simbolo
espacio
tokens
cadena
Como se puede ver, los tokens pueden ser de diversos tipos, y los atributos se deben aplicar a
cada tipo de token (no a cada token). Así, todas las cadenas deben tener los mismos atributos en el
mismo texto.
67 de 153 15/01/2018
Lazarus La Biblia del SynEdit
La forma como se agrupan los caracteres en tokens (y sus tipos), no está pre-definida por
SynEdit. No hay reglas establecidas. Cuando SynEdit realiza el resaltado de sintaxis, hace uso de un
resaltador, y es este resaltador, el que define la forma de identificar a los tokens en el texto.
Simplificando el procesos de resaltado de sintaxis, podríamos decir que consiste en, separar el
texto en tokens y darle color (atributo) a cada token. Esta descripción simplista es, sin embargo, muy
cierta, pero el proceso es más complicado de lo que suena. La mayor dificultad estriba en identificar de
forma precisa y bastante rápida a cada uno de los tokens.
Los tokens son los elementos básicos con los que trataría un analizador léxico (lexer). Un
resaltador de sintaxis es, en cierta forma, un analizador léxico, pero su objetivo no es servir de base para
un posterior análisis sintáctico o semántico, sino que bastará con identificar los tokens y sus tipos para
facilitar el resaltado6.
6
La posibilidad de convertir a un resaltador en un analizador sintáctico, queda a libertad del programador. Los
resaltadores pre-definidos en Lazarus solo identifican tokens y en algunos casos, delimitadores de bloques como
BEGIN ..END (para el plegado), pero no se diseñan con el fin de servir de “lexers”. Hacerlos de esta forma,
involucraría un procesamiento mayor y resultaría en un análisis más lento, que va en contra del objetivo de un
resaltador.
68 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Este tipo de coloreado de sintaxis implica el uso de componentes predefinidos de sintaxis. Estos
componentes vienen preparados para trabajar en un lenguaje predefinido.
El método es simple: Arrastramos el control al formulario, donde está el control SynEdit que
vamos a usar. Luego lo enlazamos, usando la propiedad “highlighter”, del SynEdit.
Posteriormente podemos elegir los colores y atributos de las palabras claves, comentarios,
números, y otras categorías, accediendo a las propiedades del objeto “TSynXXXSyn” (donde XXX
representa el lenguaje elegido). Cada control “TSynXXXSyn”, está representado por una unidad.
El uso de componentes predefinidos nos ahorra todo el trabajo de tener que procesar una
sintaxis completa de un lenguaje conocido.
Para usar este componente, solo basta con incluirlo en el formulario, como cualquier otro
componente de sintaxis. Pero antes de usarlo se le debe indicar cuales son las palabras claves que
componen su sintaxis.
SI bien este componente soporta la definición de varios lenguajes, simples, no permite mucha
flexibilidad a la hora de manejar los comentarios.
69 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Este tipo de coloreado permite personalizar de forma eficiente una sintaxis específica. Debe
usarse cuando el lenguaje a usar no existe ya, en uno de los componentes predefinidos, o no cumple con
el comportamiento requerido.
Antes de diseñar nuestra clase para el manejo del coloreado, debemos entender un poco el
funcionamiento del coloreado de sintaxis:
Cuando se asocia un resaltador, a un editor “TSynEdit”, este empezará a llamar a las rutinas del
resaltador, cada vez que requiera información sobre el coloreado de la sintaxis.
TSynEdit no guarda información del coloreado del texto en alguna parte. Siempre que requiera
información de coloreado, llamara al resaltador, para obtener, “sobre la marcha”, la información del
coloreado.
Este resaltador, debe implementar el análisis del texto y la identificación de los elementos del
texto para dar la información de los atributos del texto a SynEdit.
La exploración del texto a colorear lo hace SynEdit, procesando línea por línea. Cuando se
modifica, una línea, se repinta, la línea modificada y las siguientes líneas, a partir de la línea modificada,
que se muestren en la ventana visible. Este comportamiento en normal si se considera que puede
incluirse en una línea un elemento de sintaxis que afecte a las demás líneas del texto (como el inicio de
un comentario de varias líneas).
Al mostrar la ventana, después de haber estado oculta, se generarán los siguientes eventos:
70 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Al modificar la línea número 3 del mismo texto (un cualquier parte), la secuencia de
exploracoión cambia un poco:
Podemos ver que el editor hace una exploración de las siguientes dos líneas, y luego hace una
exploración nuevamente, pero empezando una línea anterior.
• Resaltado de comentarios de una línea. Este coloreado es típico de los comentarios de una línea de
la mayoría de lenguajes. Implica poner de un color específico, el texto de un comentario, desde el
inicio hasta el final de la línea.
• Resaltado de rangos de texto o de contexto. Este coloreado se aplica también a los comentarios, o
las cadenas. El coloreado del texto, afecta a un grupo de palabras, que pueden estar en una misma
línea u ocupar varias líneas consecutivas. Los tokens se identifican por sus delimitadores.
Cada línea se asume que está dividida en elementos llamados “tokens”. No hay parte de una
línea que no sea un token. Un token puede ser un identificador, un símbolo, un carácter de control, un
carácter en blanco, etc.
Un token puede tener uno o más caracteres de longitud. Cada tokens o tipo de token, puede
tener atributos particulares.
71 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Cada vez que SynEdit necesita procesar una línea, se produce una secuencia de llamadas al
resaltador (en realidad a “TSynCustomHighlighter”, pero este los canaliza al resaltador que se esté
usando), para obtener los atributos de cada elemento de la línea:
• Primeramente se llama el método “SetLine”, para indicar que se está empezando la exploración de
una nueva línea. Después de esta llamada espera que el método “GetTokenEx” o
“GetTokenAttribute” devuelvan información sobre el token actual.
• Después de la llamada a “SetLine”, se generarán múltiples llamadas al método “Next”, para acceder
a los siguientes tokens.
• SynEdit, espera que después de cada llamada a “Next”, los métodos “GetTokenEx” y
“GetTokenAttribute” devuelvan información sobre el token actualmente apuntado.
• Cuando SynEdit quiere verificar si se ha llegado al final de la línea de trabajo, hará una llamada a
“GetEol”. Esta debe estar funcional desde que se llama a “SetLine”.
Las llamadas a estos método se producen repetidamente y en gran cantidad para cada línea. Por
ello estos métodos, deben ser de respuesta rápida y de implementación eficiente. La demora en el
procesamiento de alguno de estos métodos afectará al rendimiento del editor.
Para tener una idea del trabajo que hace SynEdit, en cuanto a coloreado de sintaxis,
presentamos a continuación, la secuencia de métodos llamados cuando se muestra la ventana de editor,
que estaba oculta.
72 de 153 15/01/2018
Lazarus La Biblia del SynEdit
39. GetEol 44. GetTokenEx
40. GetTokenEx 45. GetTokenAttribute
41. GetTokenAttribute 46. Next:24
42. Next:18 47. GetEol
43. GetEol
El valor indicado después del Evento Next, corresponde al carácter inicial que se explora. Se
pude observar que el editor comprueba siempre si se ha llegado al final, después de cada llamada a
“Next”. Si después de una llamada a Next(), el editor obtiene TRUE en GetEol(), asumirá que está en el
carácter final de la línea, y ya no pedirá información de atributo.
SynEdit Resaltador
SetLine()
Next()
GetEol()
FALSE
GetTokenEx()
GetTokenAttribute()
Se dibuja el token en
la pantalla.
Next()
Se verifica si hay más GetEol()
tokens para dibujar FALSE
GetTokenEx()
GetTokenAttribute()
Next()
GetEol()
TRUE
El editor va dibujando en pantalla, siempre token por token. El método GetTokenEx() devuelve
la extensión del token, y el método GetTokenAttribute(), devuelve el atributo a aplicar al texto. Esa
información es todo lo que necesita SynEdit para dibujar una porción del texto con atributos, en
pantalla.
Si en nuestro texto de ejemplo realizamos una modificación, como insertar una coma al final de
la línea, se genera la siguiente secuencia:
73 de 153 15/01/2018
Lazarus La Biblia del SynEdit
14. Next:12 47. GetTokenEx
15. GetEol 48. GetTokenAttribute
16. Next:14 49. Next:11
17. GetEol 50. GetEol
18. Next:15 51. GetTokenEx
19. GetEol 52. GetTokenAttribute
20. Next:17 53. Next:12
21. GetEol 54. GetEol
22. Next:18 55. GetTokenEx
23. GetEol 56. GetTokenAttribute
24. Next:24 57. Next:14
25. GetEol 58. GetEol
26. Next:25 59. GetTokenEx
27. GetEol 60. GetTokenAttribute
28. SetLine: En un lugar de la Mancha, 61. Next:15
29. Next:0 62. GetEol
30. GetEol 63. GetTokenEx
31. GetTokenEx 64. GetTokenAttribute
32. GetTokenAttribute 65. Next:17
33. Next:2 66. GetEol
34. GetEol 67. GetTokenEx
35. GetTokenEx 68. GetTokenAttribute
36. GetTokenAttribute 69. Next:18
37. Next:3 70. GetEol
38. GetEol 71. GetTokenEx
39. GetTokenEx 72. GetTokenAttribute
40. GetTokenAttribute 73. Next:24
41. Next:5 74. GetEol
42. GetEol 75. GetTokenEx
43. GetTokenEx 76. GetTokenAttribute
44. GetTokenAttribute 77. Next:25
45. Next:6 78. GetEol
46. GetEol
Se puede notar que el editor realiza primero una exploración previa, de toda la línea, antes de
aplicar los atributos.
74 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Estos métodos son llamados de forma menos frecuente, que los métodos de coloreado de
sintaxis. Solo se ejecutan cuando el cursor apunta a un “bracket” o cuando se agrega o quita alguno.
Si ha entendido el proceso de coloreado de sintaxis, ya estamos listos para dar los primeros
pasos en la implementación de un resaltador por código.
Ante todo es recomendable crear una Unidad especial para almacenar el código de nuestro
nuevo resaltador.
Para este ejemplo crearemos una unidad llamada “uSintax“, e incluiremos las unidades
necesarias para la creación de los objetos a usar.
{
Unidad mínima que demuestra la estructura de una clase sencilla que será
usada para el resaltado de sintaxis. No es funcional, es solo demostrativa.
Creada por Tito Hinostroza: 04/08/2013
}
unit uSintax; {$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Graphics, SynEditHighlighter;
type
{Clase para la creación de un resaltador}
TSynMiColor = class(TSynCustomHighlighter)
protected
posIni, posFin: Integer;
linAct: String;
public
procedure SetLine(const NewValue: String; LineNumber: Integer); override;
procedure Next; override;
function GetEol: Boolean; override;
procedure GetTokenEx(out TokenStart: PChar; out TokenLength: integer);
override;
function GetTokenAttribute: TSynHighlighterAttributes; override;
public
function GetToken: String; override;
function GetTokenPos: Integer; override;
function GetTokenKind: integer; override;
constructor Create(AOwner: TComponent); override;
75 de 153 15/01/2018
Lazarus La Biblia del SynEdit
end;
implementation
procedure TSynMiColor.Next;
{Es llamado por SynEdit, para acceder al siguiente Token. Y es ejecutado por
cada token de la línea en curso. En este ejemplo siempre se movera un
caracter.}
begin
posIni := posFin; //apunta al siguiente token
If posIni > length(linAct) then //¿Fin de línea?
exit //salir
else
inc(posFin); //mueve un caracter
end;
76 de 153 15/01/2018
Lazarus La Biblia del SynEdit
begin
Result := nil;
end;
{Las siguientes funciones, son usadas por SynEdit para el manejo de las
llaves, corchetes, parentesis y comillas. No son cruciales para el coloreado
de tokens, pero deben responder bien.}
function TSynMiColor.GetToken: String;
begin
Result := '';
end;
end.
Esta es, probablemente, la clase más simple que se puede implementar para un resaltado de
sintaxis. Sin embargo, esta clase no resaltará ningún texto porque no contiene instrucciones para
cambiar los atributos del texto. Se limita simplemente a devolver los valores por defecto a las solicitudes
de “SynEdit”. No tiene utilidad, es simplemente un ejemplo minimalista de demostración.
Los métodos que aparecen como “override” son los que se requieren implementar para darle la
funcionalidad de coloreado, a nuestro resaltador.
Con cada llamada a “SetLine”, se guarda una copia de la cadena en “linAct”, luego se utiliza esta
cadena para ir extrayendo los tokens.
A cada petición de “Next”, esta unidad solo devuelve el siguiente carácter que se encuentra en
la línea y el atributo devuelto por “GetTokenAttribute”, es siempre NIL, que significa que no hay
atributos.
La clase de resaltador que hemos creado, se llama “TSynMiColor”. No es posible usar la misma
clase “TSynCustomHighlighter” como resaltador, porque esta clase es abstracta, y solo se usa para
canalizar apropiadamente los requerimientos de TsynEdit, al realizar el coloreado de sintaxis.
Para usar la nueva sintaxis, debemos crear un objeto y asociarla al componente TSynEdit que
vayamos a usar. Si tenemos nuestro formulario principal en Unit1 y nuestro objeto TsynEdit se llama
“editor”, entonces el código para el uso de esta sintaxis podría ser:
77 de 153 15/01/2018
Lazarus La Biblia del SynEdit
unit Unit1;
{$mode objfpc}{$H+}
interface
uses ... uSintax;
...
var
Sintaxis : TSynMiColor;
...
procedure TForm1.FormCreate(Sender: TObject);
...
Sintaxis := TSynMiColor.Create(Self); //crea resaltador
editor.Highlighter := Sintaxis; //asigna la sintaxis al editor
end;
El ejemplo anterior, se creo solo con fines didácticos. No se cumple con la funcionalidad
deseada, pero se muestra la estructura que debe tener toda clase de coloreado de sintaxis.
Para comenzar, debemos tener en mente que los métodos a implementar, deben ser de
ejecución rápida. No deben estar cargados de mucho procesamiento, porque son llamados
repetidamente para cada línea modificada del editor, así que no admiten retrasos, de otra forma el
editor se volverá pesado y lento.
Por ello no es eficiente crear una copia nueva para nosotros. Bastará con guardar una
referencia, un puntero a esta cadena, almacenada en “TSynCustomHighlighter”.
Esto implica modificar la variable “linAct”, para que sea un “PChar”, en lugar de un string. Esto
se hace en la definición de la clase. Los métodos “SetLine”, “Next”, “GetEol”, “GetTokenEx” y
“GetTokenAttribute”, también deben cambiar:
78 de 153 15/01/2018
Lazarus La Biblia del SynEdit
{
Unidad mínima que demuestra la estructura de una clase sencilla que será
usada para el resaltado de sintaxis. No es funcional, es solo demostrativa.
Creada por Tito Hinostroza: 04/08/2013
}
unit uSintax; {$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Graphics, SynEditHighlighter;
type
{Clase para la creación de un resaltador}
TSynMiColor = class(TSynCustomHighlighter)
protected
posIni, posFin: Integer;
linAct: PChar;
public
procedure SetLine(const NewValue: String; LineNumber: Integer); override;
procedure Next; override;
function GetEol: Boolean; override;
procedure GetTokenEx(out TokenStart: PChar; out TokenLength: integer);
override;
function GetTokenAttribute: TSynHighlighterAttributes; override;
public
function GetToken: String; override;
function GetTokenPos: Integer; override;
function GetTokenKind: integer; override;
constructor Create(AOwner: TComponent); override;
end;
implementation
procedure TSynMiColor.Next;
79 de 153 15/01/2018
Lazarus La Biblia del SynEdit
{Es llamado por SynEdit, para acceder al siguiente Token. Y es ejecutado por
cada token de la línea en curso. En este ejemplo siempre se movera un
caracter.}
begin
posIni := posFin; //apunta al siguiente token
if linAct[posIni] = #0 then exit; ///¿apunta al final?
inc(posFin); //mueve un caracter
end;
{Las siguientes funciones, son usadas por SynEdit para el manejo de las
llaves, corchetes, parentesis y comillas. No son cruciales para el coloreado
de tokens, pero deben responder bien.}
function TSynMiColor.GetToken: String;
begin
Result := '';
end;
end.
80 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Ahora vemos que debemos iniciar “posFin” a cero, que es donde empieza ahora la cadena, en
“linAct”.
Pero aún esta clase está vacía de atributos. Lo primero que deberíamos hacer es crear nuestros
atributos. Estos se deben declarar como propiedades del objeto “TSynMiColor”:
fAtriComent : TSynHighlighterAttributes;
fAtriIdent : TSynHighlighterAttributes;
fAtriClave : TSynHighlighterAttributes;
fAtriNumero : TSynHighlighterAttributes;
fAtriEspac : TSynHighlighterAttributes;
fAtriCadena : TSynHighlighterAttributes;
Todos los atributos, son de tipo “TSynHighlighterAttributes”. Esta clase contiene los diversos
atributos que se le pueden asociar a un token, como color de texto, color de fondo, color de borde, etc.
Notar que las constantes fsBold, fsItalic, ... están definidas en la unidad “ Graphics”.
Se han definido atributos de varias categorías de tokens. Aquí es donde se define la apariencia
que tendrá el texto de los tokens.
81 de 153 15/01/2018
Lazarus La Biblia del SynEdit
SYNS_AttrASP = 'Asp';
SYNS_AttrAssembler = 'Assembler';
SYNS_AttrAttributeName = 'Attribute Name';
SYNS_AttrAttributeValue = 'Attribute Value';
SYNS_AttrBlock = 'Block';
SYNS_AttrBrackets = 'Brackets';
SYNS_AttrCDATASection = 'CDATA Section';
SYNS_AttrCharacter = 'Character';
SYNS_AttrClass = 'Class';
SYNS_AttrComment = 'Comment';
SYNS_AttrIDEDirective = 'IDE Directive';
SYNS_AttrCondition = 'Condition';
SYNS_AttrDataType = 'Data type';
SYNS_AttrDefaultPackage = 'Default packages';
SYNS_AttrDir = 'Direction';
SYNS_AttrDirective = 'Directive';
SYNS_AttrDOCTYPESection = 'DOCTYPE Section';
...
La forma de crear un atributo, usando estas constantes, sería identificar primero el tipo de
atributo que vamos a crear y elegir la constante que mejor la describa. Para la mayoría de sintaxis, estas
serían:
SYNS_AttrComment
SYNS_AttrReservedWord
SYNS_AttrNumber
SYNS_AttrSpace
SYNS_AttrString
SYNS_AttrSymbol
SYNS_AttrDirective
SYNS_AttrAssembler
Por lo tanto, para crear un atributo para palabras claves, podríamos que hacer:
fAtriClave := TSynHighlighterAttributes.Create(SYNS_AttrReservedWord,
SYNS_XML_AttrReservedWord);
Usar constantes predefinidas, para crear los atributos, no es obligatorio, ni necesario para el
funcionamiento del resaltador, pero es una buena práctica si deseamos que nuestros resaltadores,
puedan trabajar correctamente con otros programas de Lazarus. Para más información sobre atributos,
ver la sección 2.4.3 - Atributos.
Debemos recordar que todos los elementos de la línea a explorar, debe ser necesariamente un
token, inclusive los espacios y símbolos.
El siguiente ejemplo, muestra cómo se puede dividir una cadena en tokens diversos:
82 de 153 15/01/2018
Lazarus La Biblia del SynEdit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
x p : = x p + 1 ; / / c o m e n t a r i o
En este ejemplo el primer token, se define por los caracteres 1 y 2, y se muestra en amarillo. El
segundo token es un espacio en blanco y se indica con el color verde. Los caracteres 4 y 5 pueden
considerarse como un solo token o como dos tokens distintos. El carácter 12 es un token que
seguramente estará en la categoría de números. Los caracteres 14, 15 y 16 se deben agrupar en un solo
token espacio de 3 caracteres de ancho (sería ineficiente tratarlo como 3 tokens). A partir del carácter
17, se encuentra un token que abarca hasta el fin de la línea.
Los límites del token, lo define el resaltador (que funciona como extractor de tokens o “lexer”).
El editor hará caso, sumisamente, a lo indicado por este objeto, coloreándolo de acuerdo a los atributos
entregados.
Para identificar fácilmente a los atributos, es conveniente crear una enumeración para los
atributos de tokens:
TSynMiColor = class(TSynCustomHighlighter)
...
fTokenID: TtkTokenKind; //Id del token actual
...
end;
Ahora cuando queramos asignar un atributo, al token actual, debemos poner en “fTokenID”, el
identificador del token.
Una vez creados los atributos, debemos agregar funcionalidad al método “Next”, para que
pueda extraer los tokens adecuadamente, de la línea de trabajo. La implementación debe ser lo más
eficiente posible, por ello usaremos el método de tabla de funciones o de métodos.
La idea es leer el carácter de un token, y de acuerdo a su valor ASCCI, llamamos a una función
apropiada, para tratar ese carácter. Para que la llamada sea eficiente, creamos una tabla y la llenamos
con punteros a las funciones adecuadas.
Type
TProcTableProc = procedure of object; //Tipo procedimiento para procesar el
83 de 153 15/01/2018
Lazarus La Biblia del SynEdit
TSynMiColor = class(TSynCustomHighlighter)
protected
...
fProcTable: array[#0..#255] of TProcTableProc; //tabla de funciones
...
end;
El tipo “TProcTableProc” es un método simple que define procedimientos sin parámetros (así la
llamada se hace más rápida). Este tipo de procedimiento es el que se llamará cuando se identifique el
carácter inicial de algún token.
Ahora que se tiene definido el tipo de procedimiento a usar, se debe crear estos procedimientos
de tratamientos de tokens y llenar la tabla de métodos con sus direcciones. El siguiente código es un
ejemplo sencillo de llenado de la tabla de métodos:
...
procedure TSynMiColor.CreaTablaDeMetodos;
{Construye la tabla de las funciones usadas para cada caracter inicial del
tóken a procesar. Proporciona una forma rápida de procesar un token por el
caracter inicial}
var
I: Char;
begin
for I := #0 to #255 do
case I of
'_','A'..'Z','a'..'z': fProcTable[I] := @ProcIdent;
#0 : fProcTable[I] := @ProcNull; //Caracter de marca de fin de cadena
#1..#9, #11, #12, #14..#32: fProcTable[I] := @ProcSpace;
else fProcTable[I] := @ProcUnknown;
end;
end;
Este método, hace corresponder la dirección de una función a cada una de las 256 de las
posiciones de la tabla “fProcTable[]”.
procedure TSynMiColor.ProcIdent;
//Procesa un identificador o palabra clave
begin
while linAct[posFin] in ['_','A'..'Z','a'..'z'] do
Inc(posFin);
fTokenID := tkKey;
84 de 153 15/01/2018
Lazarus La Biblia del SynEdit
end;
La cadena “linAct”, se va explorando hasta encontrar un carácter que no sea un carácter válido
para un identificador. Observar que no se considera los caracteres del código ASCII extendido (á,é,í, etc).
En este ejemplo sencillo, no se distingue el tipo de identificador, sino que se le asigna a todos el atributo
“tkKey”. Si se quisiera elegir, solo a algunas palabras para marcarlas como “tkKey”, se debe hacer aquí.
El procedimiento “ProcNull”, se llama al detectar el carácter NUL, es decir el fin de la cadena. Así
que su procesamiento solo reduce a marcar “fTokenID” como “tkNull”.
procedure TSynMiColor.ProcNull;
//Procesa la ocurrencia del caracter #0
begin
fTokenID := tkNull; //Solo necesita esto para indicar que se llegó al
final de la línea
end;
El procedimiento “ProcSpace”, permite procesar los bloques de espacios en blanco. Para los
fines de sintaxis, se considerará espacios en blanco, los primeros 32 caracteres del código ASCII,
exceptuando los caracteres #10 y #13 que corresponden a saltos de línea:
procedure TSynMiColor.ProcSpace;
//Procesa caracter que es inicio de espacio
begin
fTokenID := tkSpace;
repeat
Inc(posFin);
until (linAct[posFin] > #32) or (linAct[posFin] in [#0, #10, #13]);
end;
procedure TSynMiColor.ProcUnknown;
85 de 153 15/01/2018
Lazarus La Biblia del SynEdit
begin
inc(posFin);
while (linAct[posFin] in [#128..#191]) OR // continued utf8 subcode
((linAct[posFin]<>#0) and (fProcTable[linAct[posFin]] = @ProcUnknown)) do
inc(posFin);
fTokenID := tkUnknown;
end;
Es importante tener siempre un procedimiento de este tipo para considerar todos aquellos
tokens que no son categorizados en grupos predefinidos. Observar que también se consideran los
caracteres UTF-8 del código ASCII extendido. Esto es normal ya que SynEdIt trabaja solamente con UTF-
8.
Una vez definidos estos procedimientos básicos, se debe implementar la llamada en el método
“Next”. El código tendría la siguiente forma:
procedure TSynMiColor.Next;
//Es llamado por SynEdit, para acceder al siguiente Token.
begin
posIni := posFin; //apunta al siguiente token
fProcTable[linAct[posFin]]; //Se ejecuta la función que corresponda.
end;
Para que la sintaxis sea reconocida, solo falta modificar “GetTokenAttribute”, para indicarle al
editor, que atributo debe usar para cada token:
86 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Tal como hemos definido nuestro resaltador, se reconocerán todas las palabras como palabras
claves, y se mostrarán en color verde. Los símbolos y demás caracteres imprimibles, se mostrarán sin
atributos, es decir que tomarán el color por defecto del texto.
La siguiente pantalla muestra cómo quedaría un texto simple, usando este resaltador:
{
Unidad mínima que demuestra la estructura de una clase sencilla que será usada
para el resaltado de sintaxis. No es funcional, es solo demostrativa.
Creada por Tito Hinostroza: 04/08/2013
}
unit uSintax; {$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Graphics, SynEditHighlighter;
type
{Clase para la creación de un resaltador}
87 de 153 15/01/2018
Lazarus La Biblia del SynEdit
function GetTokenKind: integer; override;
constructor Create(AOwner: TComponent); override;
private
procedure CreaTablaDeMetodos;
procedure ProcIdent;
procedure ProcNull;
procedure ProcSpace;
procedure ProcUnknown;
end;
implementation
procedure TSynMiColor.CreaTablaDeMetodos;
{Construye la tabla de las funciones usadas para cada caracter inicial del tóken a
procesar.
Proporciona una forma rápida de procesar un token por el caracter inicial}
var
I: Char;
begin
for I := #0 to #255 do
case I of
'_','A'..'Z','a'..'z': fProcTable[I] := @ProcIdent;
#0 : fProcTable[I] := @ProcNull; //Se lee el caracter de marca de fin de
cadena
#1..#9, #11, #12, #14..#32: fProcTable[I] := @ProcSpace;
else fProcTable[I] := @ProcUnknown;
88 de 153 15/01/2018
Lazarus La Biblia del SynEdit
end;
end;
procedure TSynMiColor.ProcIdent;
//Procesa un identificador o palabra clave
begin
while linAct[posFin] in ['_','A'..'Z','a'..'z'] do
Inc(posFin);
fTokenID := tkKey;
end;
procedure TSynMiColor.ProcNull;
//Procesa la ocurrencia del caracter #0
begin
fTokenID := tkNull; //Solo necesita esto para indicar que se llegó al final de la
línae
end;
procedure TSynMiColor.ProcSpace;
//Procesa caracter que es inicio de espacio
begin
fTokenID := tkSpace;
repeat
Inc(posFin);
until (linAct[posFin] > #32) or (linAct[posFin] in [#0, #10, #13]);
end;
procedure TSynMiColor.ProcUnknown;
begin
inc(posFin);
while (linAct[posFin] in [#128..#191]) OR //continued utf8 subcode
((linAct[posFin]<>#0) and (fProcTable[linAct[posFin]] = @ProcUnknown)) do
inc(posFin);
fTokenID := tkUnknown;
end;
procedure TSynMiColor.Next;
//Es llamado por SynEdit, para acceder al siguiente Token.
begin
posIni := posFin; //apunta al siguiente token
fProcTable[linAct[posFin]]; //Se ejecuta la función que corresponda.
end;
89 de 153 15/01/2018
Lazarus La Biblia del SynEdit
begin
Result := fTokenId = tkNull;
end;
{Las siguientes funciones, son usadas por SynEdit para el manejo de las
llaves, corchetes, parentesis y comillas. No son cruciales para el coloreado
de tokens, pero deben responder bien.}
function TSynMiColor.GetToken: String;
begin
Result := '';
end;
end.
Puede que alguien se haya preguntado ¿Cómo acceder, desde fuera de la clase, a los atributos
de, por ejemplo, las palabras claves? Recordemos que los atributos de los tokens se deben declarar en el
resaltador y no en la clase padre “TSynCustomHighlighter”.
Una respuesta sencilla, sería “ponemos las propiedades de atributo como públicos, y luego
podremos referenciarlo como cualquier, propiedad de nuestro resaltador.
90 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Y es cierto, eso funcionaría, pero si la pregunta fuera: ¿Cómo acceder desde el editor a los
atributos de los tokens? Entonces ahí si se complica un poco la situación, porque, a pesar de que el
editor (de la clase TSyenEdit) tiene la propiedad “HighLighter”, esta solo hace referencia a la clase
“TSynCustomHighlighter” y no a la clase derivada (resaltador) que siempre usamos para implementar el
coloreado.
Para solventar, en parte, esta dificultad, existe un método adicional que es recomendable
implementar. Este método es “GetDefaultAttribute” y permitirá a nuestro resaltador, responder a las
peticiones de acceso a los atributos que genere “TSynCustomHighlighter”.
Que permiten leer o modificar los atributos indicados. Sin embargo, para que estas propiedades
funcionen, nosotros debemos sobre-escribir (override) en nuestro resaltador, el siguiente método:
Como se ve, la idea es darle acceso a nuestros atributos, de acuerdo al tipo de atributo,
solicitado. Desde luego, si no hemos definido un atributo específico, podríamos devolver NIL. De la
misma forma, es posible que hayamos definido atributos adicionales que podrían no ser accesibles
desde fuera de la clase, porque no se encuentran en la categoría solicitada.
91 de 153 15/01/2018
Lazarus La Biblia del SynEdit
En el ejemplo anterior marcamos a todos los identificadores como palabras claves asignándole
el atributo “tkKey”. Esto lo hacíamos en el método “ProcIdent”:
procedure TSynMiColor.ProcIdent;
//Procesa un identificador o palabra clave
begin
while linAct[posFin] in ['_','A'..'Z','a'..'z'] do
Inc(posFin);
fTokenID := tkKey;
end;
Pero en un caso normal, solo se marcarán algunos identificadores como palabras claves. Para
ello, el camino más sencillo podría ser, comparar el token actual, con un grupo de palabras claves, y solo
en caso de que coincidan, marcarlas como palabras claves.
procedure TSynMiColor.ProcIdent;
//Procesa un identificador o palabra clave
var tam: integer;
begin
while linAct[posFin] in ['_','A'..'Z','a'..'z'] do
Inc(posFin);
tam := posFin - posIni;
if strlcomp(linAct + posIni,'EN',tam) = 0 then fTokenID := tkKey else
if strlcomp(linAct + posIni,'DE',tam) = 0 then fTokenID := tkKey else
if strlcomp(linAct + posIni,'LA',tam) = 0 then fTokenID := tkKey else
if strlcomp(linAct + posIni,'LOS',tam) = 0 then fTokenID := tkKey else
fTokenID := tkUnknown; //identificador común
end;
En este código se reconocen solo las palabras “EN”, “LA” Y “DE” como palabras reservadas. Al
aplicar esta modificación podríamos tener una pantalla como esta.
92 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Observar que las rutinas solo reconocen las mayúsculas, porque la comparación de cadenas se
hace de esta forma.
Para ir agregando más palabras, se puede ir aumentando la lista y elegir los atributos dadas a
cada categoría de palabras. Sin embargo, este método se vuelve pesado, conforme crece la cantidad de
palabras y condicionales a agregar y por lo tanto no es el camino ideal a seguir en la implementación de
una sintaxis adecuada.
Analizando el código anterior se puede ver que el procedimiento “ProcIdent”, es el más pesado
en cuanto a procesamiento. Por su implementación requiere hacer múltiples comparaciones y
verificaciones para detectar los identificadores a colorear.
A pesar, de que el uso de conjuntos resulta eficiente, este código puede optimizarse
considerablemente si se usa una tabla de selección.
Ahora creamos un procedimiento de llenado que marque solo las casillas de caracteres válidos
para identificadores, como “true”:
procedure CreaTablaIdentif;
var
i: Char;
begin
93 de 153 15/01/2018
Lazarus La Biblia del SynEdit
for I := #0 to #255 do
begin
Case i of
'_', '0'..'9', 'a'..'z', 'A'..'Z': Identifiers[i] := True;
else
Identifiers[i] := False;
end;
end;
end;
Una vez llenada esta tabla, ya podemos usarla para detectar rápidamente, que caracteres se
consideran como parte de un identificador, usando nuevamente un simple while:
procedure TSynMiColor.ProcIdent;
//Procesa un identificador o palabra clave
var tam: integer;
begin
while Identifiers[linAct[posFin]] do Inc(posFin);
tam := posFin - posIni;
if strlcomp(linAct + posIni,'EN',tam) = 0 then fTokenID := tkKey else
if strlcomp(linAct + posIni,'DE',tam) = 0 then fTokenID := tkKey else
if strlcomp(linAct + posIni,'LA',tam) = 0 then fTokenID := tkKey else
if strlcomp(linAct + posIni,'LOS',tam) = 0 then fTokenID := tkKey else
fTokenID := tkUnknown; //identificador común
end;
El problema se reduce en optimizar la comparación de una cadena en una lista de varias. Existen
diversos métodos para llevar a cabo la optimización de esta tarea.
La mayoría de los componentes de sintaxis de Lazarus, usan el método de las funciones Hash
(Hash-functions) que es un poco complejo, pero que básicamente se trata en asignarle a cada palabra
clave, a detectar, un valor numérico, más o menos único, que permita categorizarlo en un número
pequeño de grupos (Ver Apéndice para más detalle sobre este método).
Aunque este método es rápido, no es legible y confunde fácilmente. Además, las modificaciones
sencillas, como agregar una nueva palabra clave, requiere de un cálculo cuidadoso antes de modificar el
código.
Aquí usaremos un método que es generalmente más rápido y mucho más legible y fácil de
modificar. Este algoritmo es una forma simplificada de un árbol de prefijos. Se puede ver como un árbol
en el que se implementa solo el primer nivel. Como prefijo se usa el primer carácter del identificador a
buscar. A este método lo llamaremos el algoritmo del Primer carácter como prefijo.
94 de 153 15/01/2018
Lazarus La Biblia del SynEdit
El método se implementa creando una primera categorización de las palabras usando la misma
tabla de métodos creada en “CreaTablaDeMetodos”, creando una función para cada letra inicial del
identificador. Así el código de “CreaTablaDeMetodos”, tendría la siguiente forma:
procedure TSynMiColor.CreaTablaDeMetodos;
var
I: Char;
begin
for I := #0 to #255 do
case I of
...
...
'H','h': fProcTable[I] := @ProcH;
end;
Luego en los procedimientos ProcA, ProcB, ... etc, se realiza el procesamiento de un grupo
reducido de identificadores, reduciendo sensiblemente la cantidad de comparaciones.
Por ejemplo, el procedimiento encargado de identificar las palabras claves, empezadas en “L”,
sería:
procedure TSynMiColor.ProcL;
var tam: integer;
begin
while Identifiers[linAct[posFin]] do Inc(posFin);
tam := posFin - posIni;
if strlcomp(linAct + posIni,'LA',tam) = 0 then fTokenID := tkKey else
if strlcomp(linAct + posIni,'LOS',tam) = 0 then fTokenID := tkKey else
fTokenID := tkUnknown; //sin atributos
end;
95 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Tal como está este procedimiento, solo detectará las palabras reservadas en mayúscula7. Para
hacerlo insensible a la caja, se debería agregar un procesamiento adicional.
Aprovechamos esta funcionalidad faltante para optimizar las comparaciones, usando una
función de comparación rápida que, además ignore la caja (mayúscula o minúscula). Para ello usaremos
nuevamente la invaluable ayudad de las tablas. EL método consistirá en crear una tabla que asigne un
ordinal a cada carácter alfabético, independientemente de su caja. A esta tabla la llamaremos
“mHashTable”, y aprovecharemos para llenarla en “CreaTablaIdentif”:
procedure CreaTablaIdentif;
var
i, j: Char;
begin
for i := #0 to #255 do
begin
Case i of
'_', '0'..'9', 'a'..'z', 'A'..'Z': Identifiers[i] := True;
else Identifiers[i] := False;
end;
j := UpCase(i);
Case i in ['_', 'A'..'Z', 'a'..'z'] of
True: mHashTable[i] := Ord(j) - 64
else
mHashTable[i] := 0;
end;
end;
end;
Ahora con esta función creada, ya podemos crear una función, para comparaciones rápidas.
Usaremos la que se usan en las librerías de Lazarus:
7
También se puede ver que el reconocimiento de palabras no es efectivo porque reconocerá las palabras aunque
solo coincidan en los primeros caracteres.
96 de 153 15/01/2018
Lazarus La Biblia del SynEdit
Result := False;
break;
end;
inc(Temp);
end;
end else Result := False;
end;
Esta función de comparación usa el puntero “fToIdent” y la variable “fStringLen”, para evaluar la
comparación. El único parámetro que requiere es la cadena a comparar.
procedure TSynMiColor.ProcL;
begin
while Identifiers[linAct[posFin]] do inc(posFin);
fStringLen := posFin - posIni - 1; //calcula tamaño - 1
fToIdent := linAct + posIni + 1; //puntero al identificador + 1
if KeyComp('A') then fTokenID := tkKey else
if KeyComp('OS') then fTokenID := tkKey else
fTokenID := tkUnknown; //sin atributos
end;
Este procedimiento verifica si se detectan las palabras “LA” o “LOS”. Como una optimización
adicional, se omite la comparación del primer carácter, ya que este se ha detectado antes de llamar a
esta función.
Ahora ya tenemos lista nuestro resaltador básico. Una prueba del programa con unas palabras
claves más, nos dará el siguiente resultado:
97 de 153 15/01/2018
Lazarus La Biblia del SynEdit
procedure TSynMiColor.CreaTablaDeMetodos;
var
I: Char;
begin
for I := #0 to #255 do
case I of
...
'-' : fProcTable[I] := @ProcMinus;
...
end;
end;
procedure TSynMiColor.ProcMinus;
//Procesa el símbolo '-'
begin
case LinAct[PosFin + 1] of //ve siguiente caracter
'-': //es comentario de una sola línea
begin
fTokenID := tkComment;
inc(PosFin, 2); //salta a siguiente token
while not (linAct[PosFin] in [#0, #10, #13]) do Inc(PosFin);
end;
else //debe ser el operador "menos".
begin
inc(PosFin);
fTokenID := tkUnknown;
end;
end
end;
Es necesario ver el siguiente carácter, para determinar si se trata del token de comentario. De
ser así, se busca el final de línea, para considerar todo ese bloque como un solo token con atributo
“tkComent”.
98 de 153 15/01/2018
Lazarus La Biblia del SynEdit
procedure TSynMiColor.CreaTablaDeMetodos;
var
I: Char;
begin
for I := #0 to #255 do
case I of
...
'"' : fProcTable[I] := @ProcString;
...
end;
end;
procedure TSynMiColor.ProcString;
//Procesa el caracter comilla.
begin
fTokenID := tkString; //marca como cadena
Inc(PosFin);
while (not (linAct[PosFin] in [#0, #10, #13])) do begin
if linAct[PosFin] = '"' then begin //busca fin de cadena
Inc(PosFin);
if (linAct[PosFin] <> '"') then break; //si no es doble comilla
end;
Inc(PosFin);
end;
99 de 153 15/01/2018
Lazarus La Biblia del SynEdit
end;
Se puede deducir también que el token de cadena termina necesariamente en la misma línea
donde empezó. Es posible generar coloreado de cadenas de múltiples líneas, como si fueran
comentarios multi-líneas, que es el caso que veremos a continuación.
El uso de rangos permite colorear elementos que pudieran extenderse hasta más allá de una
línea. Tal es el caso de los comentarios o cadenas de múltiples líneas que implementan muchos
lenguajes.
Para implementar esta funcionalidad, el editor maneja tres métodos que están definidos en la
clase “TSynCustomHighlighter” de la unidad “SynEditHighlighter”:
TSynCustomHighlighter = class(TComponent)
...
function GetRange: Pointer; virtual;
procedure SetRange(Value: Pointer); virtual;
procedure ResetRange; virtual;
...
end;
procedure TSynCustomHighlighter.ResetRange;
begin
end;
• ResetRange.- Se ejecuta antes de explorar la primera línea del texto, ya que no hay líneas
anteriores que afecten el rango.
Cada vez que se modifica una parte del documento, SynEdit hace diversas llamadas a
GetRange() y SetRange(), para reconstruir el nuevo estado del documento. La exploración, puede ir
desde la línea actual, hasta el final del documento, si SynEdit lo juzga necesario. Sin embargo, lo común
es que la exploración del documento solo se haga en unas pocas líneas.
El siguiente ejemplo muestra un editor, y las llamadas a los métodos de rangos y “SetLine”,
cuando se modifica la línea 3:
1. SetRange
2. SetLine: no quiero acordarme,
3. GetRange
4. SetLine: no ha mucho tiempo
5. GetRange
6. SetLine: que vivía un hidalgo
7. GetRange
8. SetRange
9. SetLine: de cuyo nombre
10. SetRange
11. SetLine: no quiero acordarme,
12. SetRange
13. SetLine: no ha mucho tiempo
14. SetRange
15. SetLine: que vivía un hidalgo
El editor suele explorar el texto desde una línea antes de la línea modificada, hasta encontrar
que una línea devuelve el mismo nivel que tenía anteriormente.
El valor que envía “SetRange”, en el parámetro “Value”, es un puntero, así como el valor que
espera recibir “GetRange”, porque han sido diseñados para trabajar con objetos. Pero no es necesario
trabajar con punteros. En la práctica se suele usar un tipo enumerado para identificar los niveles de los
rangos, teniendo cuidado de hacer las conversiones necesarias.
Type
...
TRangeState = (rsUnknown, rsComment);
...
TSynMiColor = class(TSynCustomHighlighter)
...
fRange: TRangeState;
...
end;
...
function TSynMiColor.GetRange: Pointer;
begin
Result := Pointer(PtrInt(fRange));
end;
Las funciones PtrInt y PtrUInt, convierten un puntero a un entero del mismo tamaño que el
puntero.
El valor de los punteros no es importante en sí8, porque no se hace acceso a los objetos
apuntados por ellos, en condiciones normales. Lo importante es que tomen valores diferentes cuando
se encuentra un rango particular (comentarios, bloques, etc.), de cuando no hay rangos activos.
8
En el diseño de “TSynCustomHighlighter”, se ha definido el uso de punteros, con el fin de poder usar referencias a
objetos reales, que puedan asociarse a un rango específico. Para la mayoría de casos, nos bastará con manejar un
simple entero o enumerado. Sin embargo, en ciertos desarrollos, como la clase “TSynCustomFoldHighlighter”, si se
hace uso de objetos para su funcionalidad. El uso de punteros para manejar rangos, resulta confuso al principio,
pues queda la sensación de que hubiera bastado con usar un simple entero, pero el diseño actual permite mayor
libertad.
1. SetRange: 0
2. SetLine: /*no quiero acordarme,
3. GetRange: 1
4. SetLine: no quiero acordarme,
5. GetRange: 1
6. SetLine: no ha mucho tiempo*/
7. GetRange: 0
8. SetRange: 0
9. SetLine: de cuyo 'nombre'
10. SetRange: 0
11. SetLine: /*no quiero acordarme,
12. SetRange: 1
13. SetLine: no quiero acordarme,
14. SetRange: 1
15. SetLine: no ha mucho tiempo*/
Al lado de “SetRange” o “GetRange”, se está mostrando el ordinal de “fRange”, como una ayuda
visual para ver cómo va cambiando. Aclaramos que el cambio en “fRange” no tiene por qué ser
consecutivo, basta con que “fRange” tome valores diferentes, para que la funcionalidad de coloreado,
trabaje.
El concepto de rangos, puede tornarse un poco difícil de asimilar, al principio. Puede ayudar
pensar en ellos como una forma de guardar información adicional que le corresponda a cada línea. Por
lo tanto existe un valor de rango (puntero a un objeto), por cada línea del texto explorado.
Puede haber mucha información adicional, que se quiera asociar a cada línea. Los rangos son
una forma útil de guardar esta información. El editor lee al inicio (a través del resaltador), el rango de
cada línea, al terminar de explorar la línea y guarda esta información.
Luego cuando se modifica el texto, el editor hace exploraciones sucesivas, para “actualizar” los
rangos en las líneas que pueden verse afectadas.
9
Es posible que se le pueda dar ese significado, en una implementación particular, pero no es obligatorio.
Por norma, el rango de las líneas anteriores no se afecta por el cambio en una línea cualquiera.
Pero las líneas sucesivas si pueden alterarse en su rango. Por ello, de ser el caso, el editor explorará las
líneas siguientes hasta que encuentre que, el rango que le corresponde es similar al que tenía, y cesa la
actualización.
Si es que la información que queremos asociar, no se refiere a una línea, sino a elementos más
pequeños, los rangos no ayudarán directamente10. Son más útiles, cuando la información a almacenar,
se puede obtener directa o fácilmente a partir del estado de la línea anterior.
“Conocer el estado final de la línea anterior (rango u objeto apuntado por el rango), es todo lo
que se necesita para trabajar correctamente con la línea actual”
Otra forma de ver la utilidad de los rangos, es pensar en ellos como una ayuda para trasladar
información de una línea a otra, considerando que el editor no explora ordenadamente todo el texto11,
sino que trata de hacer la menor cantidad de exploraciones, realizando varias exploraciones pequeñas
en diversas partes del texto, de acuerdo al texto modificado.
Esto significa que si alteramos por ejemplo, la variable XXX, mientras se explora la línea “n”, no
podemos estar seguros de que el valor de XXX, se leerá al explorar la línea “n+1”, porque no
necesariamente la siguiente línea a explorar será la “n+1”. Si quisiéramos que al explorarse la línea
“n+1”, se vea el valor deseado de la variable XXX, se debe ver la forma de guardarla y recuperarla como
parte del rango.
El coloreado de contexto, involucra considerar un intervalo del texto, que puede estar en una o
varias líneas, como si fuera un único token. Lógicamente, por la jerarquía usada, un token, no puede ser
mayor a una línea, por ello, si el rango se extiende a más de una línea, se identificará como varios tokens
(uno por cada línea) de la misma categoría.
10
Aunque podría idearse formas que permitan reconstruir la información del interior de la línea, en un objeto
rango.
11
Solo dentro de una línea, los tokens son explorados, siempre de forma secuencial hasta el final de la línea.
Dada nuestra clase, este coloreado se puede hacer usando una clase derivada del resaltador
usado, o se puede incluir la funcionalidad en el mismo código de la sintaxis original.
Si se desea crear una clase derivada, debe tener una estructura parecida a esta:
TSynDemoHlContextFoldBase = class(TSynMiColor)
protected
FCurRange: Integer;
public
procedure Next; override;
function GetTokenAttribute: TSynHighlighterAttributes; override;
public
Para mayor información, se recomienda ver el ejemplo que viene con Lazarus: en
\examples\SynEdit\NewHighlighterTutorial\
Sin embargo, la forma más eficiente, sería incluir esta funcionalidad en el mismo resaltador.
Ahora vamos a considerar el caso de colorear un rango de una o varias líneas. Para este ejemplo
consideremos el coloreado de comentarios de múltiples líneas.
Primero elegimos los caracteres delimitadores de comentarios. Para nuestro ejemplo usaremos
los típicos caracteres de C: “/*” y “*/”. Todo el texto que se encuentre entre estos caracteres será
considerado como un comentario y tendrá el atributo “fStringAttri”.
El delimitador de inicio debemos detectarlo por el carácter “/”, pero haciendo la validación, ya
que podría tratarse del operador de división.
procedure TSynMiColor.CreaTablaDeMetodos;
var
I: Char;
begin
for I := #0 to #255 do
case I of
...
'/' : fProcTable[I] := @ProcSlash;
...
end;
end;
procedure TSynMiColor.ProcSlash;
//Procesa el símbolo '/'
begin
case linAct[PosFin + 1] of
'*': //comentario multi-línea
begin
fRange := rsComment; //marca rango
inc(PosFin, 2);
CommentProc; //Procesa en modo comentario
end;
else //debe ser el operador "entre".
begin
inc(PosFin);
fTokenID := tkUnknown;
end;
end
end;
Observamos que estamos trabajando con un procedimiento “CommentProc” y con una nueva
bandera, llamada “fRange”. Que se debe declarar como se muestra:
Type
...
TRangeState = (rsUnknown, rsComment);
...
TSynMiColor = class(TSynCustomHighlighter)
...
fRange: TRangeState;
...
TSynMiColor = class(TSynCustomHighlighter)
...
...
Pero hace falta aún, procesar las líneas que estén en el rango de comentarios. Para ello,
implementamos el método “CommentProc”:
procedure TSynMiColor.CommentProc;
begin
fTokenID := tkComment;
case linAct[PosFin] of
#0:
begin
ProcNull;
exit;
end;
end;
while linAct[PosFin] <> #0 do
case linAct[PosFin] of
'*':
if linAct[PosFin + 1] = '/' then
begin
inc(PosFin, 2);
fRange := rsUnknown;
break;
end
else inc(PosFin);
#10: break;
#13: break;
else inc(PosFin);
end;
end;
Este método, explora las líneas en busca del delimitador final del comentario. Si no lo encuentra
considera todo lo explorado (inclusive la línea completa) como un solo token de tipo “tkComment”. Hay
que notar que no se está detectando el delimitador de fin de comentario “*/” en ninguna otra parte de
la clase.
Que debe ser llamado cuando se detecte que estamos en medio de un comentario, como se
hace en “ProcSlash”, pero debemos también incluirlo en “Next”:
procedure TSynMiColor.Next;
begin
posIni := PosFin; //apunta al primer elemento
if fRange = rsComment then
CommentProc
else
begin
fRange := rsUnknown;
fProcTable[linAct[PosFin]]; //Se ejecuta la función que corresponda.
end;
end;
Hasta ahora, se ha mostrado como implementar un resaltador simple, usando solamente las
propiedades y métodos necesarios de TSynCustomHighlighter. Ahora vamos a hablar un poco de la
clase en sí y de algunas funcionalidades adicionales que trae.
Una rápida mirada al código de la clase, nos indicará que es una clase más o menos extensa,
para ser una clase abstracta. Pero, a pesar de todo, la clase TSynCustomHighlighter, no almacena
información en sí. Toda la información que maneja, se guarda en el editor, específicamente en Lines[].
Todo editor que desee implementar el resaltado de sintaxis, debe estar asociado a un
resaltador. Las relaciones entre un editor y un resaltador son:
Esta relación, se puede deducir, tomando en consideración que el resaltador en sí, no almacena
información del texto que explora.
La información de rangos, que genera el resaltador, la guarda en el editor y la usa para acceder a
cada línea, con el estado inicial apropiado.
El arreglo CurrentLines[], es asignado a Lines[] antes que el editor use el resaltador, así que
siempre se garantizará que CurrentLines[], hace referencia al editor actual.
De la misma forma a como CurrentLines[], nos permite acceder a la información de las líneas
actuales, la propiedad CurrentRanges[]12, nos permite acceder a los valores de rango, que le
corresponde a cada línea del texto.
12
Hay que aclarar que CurrentRanges[], se encuentra declarado como PROTECTED, así que no es directamente
accesible desde fuera de la clase, pero se puede hacer accesible al momento de crear nuestro propio resaltador.
CurrentRanges[], es un objeto complejo, pero para fines prácticos, lo podemos ver como una
simple tabla de punteros. Estos valores, son los que se asignan a cada línea, cuando
TSynCustomHighlighter, lo solicita al resaltador mediante GetRange().
Es necesario saber que CurrentLines[] y CurrentRanges[], son como tablas que empiezan en
cero. Si por ejemplo quisiéramos ver el valor de rango de la línea 3, tendríamos que ver en
CurrentRanges[2].
• TSynCustomHighlighter.GetRange()
• TSynCustomHighlighter.SetRange()
Es decir que CurrentRanges[] nos permite recuperar la información del Rango almacenada en
cada línea .
TSynCustomHighlighter
CurrentLines[] CurrentRanges[]
0 String 0 Pointer
1 String 1 Pointer
2 String 2 Pointer
3 String 3 Pointer
TSynEdit 4 String 4 Pointer
5 String 5 Pointer
6 String 6 Pointer
7 String 7 Pointer
n String n Pointer
Desde luego que el significado que tenga este puntero, es el mismo que se usa en el resaltador
(enumerado, entero, referencia a objeto, etc). Corresponderá al programador, aplicar la conversión de
tipos, correspondiente a cada caso.
El método ScanAllRanges(), hace que el editor explore de nuevo, todas las líneas para actualizar
las referencias al rango que tiene cada línea. Este método es útil, por ejemplo, cuando se cambia la
sintaxis del lenguaje de trabajo.
Existe una propiedad interna que puede usarse para el trabajo con identificadores: IdentChars.
Internamente, es una simple referencia a GetIdentChars(), que tiene la siguiente definición:
Puede que sea necesario, sobrescribir este método para que se adapte a nuestras necesidades.
El método StartAtLineIndex(), permite posicionar el resaltador en una línea cualquiera del texto,
para iniciar la exploración de esa línea. Esto es útil cundo queremos realizar una exploración adicional
(fuera del proceso normal de exploración del resaltador), para obtener alguna información adicional del
resaltador.
2.4.3 Atributos
Gran parte del código de TSynCustomHighlighter, está referido al manejo de atributos. Estos
permiten configurar como se verán los tokens en el editor.
Los atributos son objetos que se deberían crear antes de usar el resaltador. Para crear un
atributo nuevo, se ejecuta:
fKeyAttri := TSynHighlighterAttributes.Create(SYNS_AttrKey,
SYNS_XML_AttrKey);
fKeyAttri.Style := [fsBold];
AddAttribute(fKeyAttri); //guarda la referencia al atributo
Por lo general, este código se coloca siempre en el constructor del resaltador. Pero podrían
crearse dinámicamente en cualquier parte del proceso.
Las referencias a los atributos se guardan en una estructura interna de la clase (fAttributes).
Como facilidad adicional, se liberan al destruir la clase, así que no es necesario (ni se debe), destruir los
atributos agregados al resaltador.
Todos los resaltadores, tienen definidos (de forma estática o dinámica) varios atributos, porque
son necesarios para asignar las propiedades de resaltado del texto.
TSynCustomHighlighter = class(TComponent)
...
public
property AttrCount: integer read GetAttribCount;
property Attribute[idx: integer]: TSynHighlighterAttributes
read GetAttribute;
property Capabilities: TSynHighlighterCapabilities
read {$IFDEF SYN_LAZARUS}FCapabilities{$ELSE}GetCapabilities{$ENDIF};
property SampleSource: string read GetSampleSource write SetSampleSource;
property CommentAttribute: TSynHighlighterAttributes
index SYN_ATTR_COMMENT read GetDefaultAttribute;
property IdentifierAttribute: TSynHighlighterAttributes
index SYN_ATTR_IDENTIFIER read GetDefaultAttribute;
property KeywordAttribute: TSynHighlighterAttributes
index SYN_ATTR_KEYWORD read GetDefaultAttribute;
property StringAttribute: TSynHighlighterAttributes
index SYN_ATTR_STRING read GetDefaultAttribute;
property SymbolAttribute: TSynHighlighterAttributes
//mh 2001-09-13
index SYN_ATTR_SYMBOL read GetDefaultAttribute;
property WhitespaceAttribute: TSynHighlighterAttributes
index SYN_ATTR_WHITESPACE read GetDefaultAttribute;
const
SYN_ATTR_COMMENT = 0;
SYN_ATTR_IDENTIFIER = 1;
SYN_ATTR_KEYWORD = 2;
SYN_ATTR_STRING = 3;
SYN_ATTR_WHITESPACE = 4;
SYN_ATTR_SYMBOL = 5;
De lo visto sobre los atributos, se puede deducir que existen diversas formas para acceder a los
atributos de un resaltador.
Sin embargo, esta forma solo funcionará para los atributos que hayan sido correctamente
inscritos en el método GetDefaultAttribute(), lo cual depende de la correcta implementación del
resaltador. Además solo son visibles algunos atributos más comunes.
Otra forma de acceso a los atributos es usar las propiedades públicas que implementan muchos
resaltadores. Estas son:
TSynLFMSyn = class(TSynCustomFoldHighlighter)
...
private
fCommentAttri: TSynHighlighterAttributes;
fIdentifierAttri: TSynHighlighterAttributes;
fKeyAttri: TSynHighlighterAttributes;
fNumberAttri: TSynHighlighterAttributes;
fSpaceAttri: TSynHighlighterAttributes;
fStringAttri: TSynHighlighterAttributes;
fSymbolAttri: TSynHighlighterAttributes;
...
published
property CommentAttri: TSynHighlighterAttributes read fCommentAttri
write fCommentAttri;
property IdentifierAttri: TSynHighlighterAttributes read fIdentifierAttri
write fIdentifierAttri;
property KeyAttri: TSynHighlighterAttributes read fKeyAttri write
fKeyAttri;
property NumberAttri: TSynHighlighterAttributes read fNumberAttri
write fNumberAttri;
property SpaceAttri: TSynHighlighterAttributes read fSpaceAttri
write fSpaceAttri;
property StringAttri: TSynHighlighterAttributes read fStringAttri
write fStringAttri;
end;
Los atributos en sí están ocultos, pero sus propiedades respectivas están publicadas, así que
podrían cambiarse desde el Inspector de Objetos.
Así una forma de acceder a los atributos, usando estas propiedades sería:
Otra forma de acceder a todos los atributos del resaltador sería usando la tabla de atributos
Attribute[], que es una simple referencia al método protegido GetAttribute():
...
atributo := SynLFMSyn1.Attribute[2];
atributo.Foreground:=clRed;
La desventaja, es que necesitamos conocer el índice del atributo con el que vamos a trabajar.
Como ayuda podríamos usar el nombre del atributo.
var i:integer;
begin
...
for i:=0 to SynLFMSyn1.AttrCount - 1 do
ShowMessage(SynLFMSyn1.Attribute[i].Name);
...
Atributo := TSynHighlighterAttributes.Create('Nombre');
Que permite asignar otro nombre interno al atributo. Luego podremos acceder a este nombre
mediante la propiedad “StoredName” del atributo.
Iterar a través de Attribute[], permite acceder efectivamente a todos los atributos que se hayan
creado en el resaltador.
Marcas de
plegado
Vamos a considerar agregar plegado de código, partiendo de un resaltador sencillo, como el que
hemos descrito anteriormente.
TSynMiColor = class(TSynCustomHighlighter)
...
Por:
TSynDemoHlFold = class(TSynCustomFoldHighlighter)
...
inherited;
fRange := rsUnknown;
end;
Una vez implementadas estas modificaciones, ya estamos listos para agregar el “folding”, para
ello debemos detectar, el inicio y final del bloque.
Por ejemplo, si agregáramos el inició con la detección de la palabra BEGIN y el fin del bloque con
la palabra END, el código sería:
procedure TSynMiColor.ProcB;
begin
while Identifiers[linAct[posFin]] do inc(posFin);
fStringLen := posFin - posIni - 1; //calcula tamaño - 1
fToIdent := linAct + posIni + 1; //puntero al identificador + 1
if KeyComp('EGIN') then begin
fTokenID := tkKey; StartCodeFoldBlock(nil); end
else
if KeyComp('Y') then fTokenID := tkKey else
fTokenID := tkUnknown; //identificador común
end;
...
procedure TSynMiColor.ProcE;
begin
while Identifiers[linAct[posFin]] do inc(posFin);
fStringLen := posFin - posIni - 1; //calcula tamaño - 1
fToIdent := linAct + posIni + 1; //puntero al identificador + 1
if KeyComp('N') then fTokenID := tkKey else
if KeyComp('ND') then begin
fTokenID := tkKey; EndCodeFoldBlock(); end
else
procedure ProcB;
procedure ProcC;
procedure ProcD;
procedure ProcE;
procedure ProcL;
implementation
var
Identifiers: array[#0..#255] of ByteBool;
mHashTable: array[#0..#255] of Integer;
procedure CreaTablaIdentif;
var i, j: Char;
begin
for i := #0 to #255 do
begin
Case i of
'_', '0'..'9', 'a'..'z', 'A'..'Z': Identifiers[i] := True;
else Identifiers[i] := False;
end;
j := UpCase(i);
Case i in ['_', 'A'..'Z', 'a'..'z'] of
True: mHashTable[i] := Ord(j) - 64
procedure TSynMiColor.CreaTablaDeMetodos;
var
I: Char;
begin
for I := #0 to #255 do
case I of
'-' : fProcTable[I] := @ProcMinus;
'"' : fProcTable[I] := @ProcString;
'/' : fProcTable[I] := @ProcSlash;
'B','b': fProcTable[I] := @ProcB;
'C','c': fProcTable[I] := @ProcC;
'D','d': fProcTable[I] := @ProcD;
'E','e': fProcTable[I] := @ProcE;
'L','l': fProcTable[I] := @ProcL;
#0 : fProcTable[I] := @ProcNull; //Se lee el caracter de marca de fin de
cadena
#1..#9, #11, #12, #14..#32: fProcTable[I] := @ProcSpace;
else fProcTable[I] := @ProcUnknown;
end;
end;
function TSynMiColor.KeyComp(const aKey: String): Boolean;
var
I: Integer;
Temp: PChar;
procedure TSynMiColor.ProcMinus;
//Procesa el símbolo '-'
begin
case LinAct[PosFin + 1] of //ve siguiente caracter
'-': //es comentario de una sola línea
begin
fTokenID := tkComment;
inc(PosFin, 2); //salta a siguiente token
while not (linAct[PosFin] in [#0, #10, #13]) do Inc(PosFin);
end;
else //debe ser el operador "menos".
begin
inc(PosFin);
fTokenID := tkUnknown;
end;
end
end;
procedure TSynMiColor.ProcString;
//Procesa el caracter comilla.
begin
fTokenID := tkString; //marca como cadena
Inc(PosFin);
while (not (linAct[PosFin] in [#0, #10, #13])) do begin
if linAct[PosFin] = '"' then begin //busca fin de cadena
Inc(PosFin);
if (linAct[PosFin] <> '"') then break; //si no es doble comilla
end;
Inc(PosFin);
end;
end;
procedure TSynMiColor.ProcSlash;
//Procesa el símbolo '/'
begin
case linAct[PosFin + 1] of
'*': //comentario multi-línea
begin
fRange := rsComment; //marca rango
fTokenID := tkComment;
procedure TSynMiColor.ProcB;
begin
while Identifiers[linAct[posFin]] do inc(posFin);
fStringLen := posFin - posIni - 1; //calcula tamaño - 1
fToIdent := linAct + posIni + 1; //puntero al identificador + 1
if KeyComp('EGIN') then begin fTokenID := tkKey; StartCodeFoldBlock(nil); end else
if KeyComp('Y') then fTokenID := tkKey else
fTokenID := tkUnknown; //identificador común
end;
procedure TSynMiColor.ProcC;
begin
while Identifiers[linAct[posFin]] do inc(posFin);
fStringLen := posFin - posIni - 1; //calcula tamaño - 1
fToIdent := linAct + posIni + 1; //puntero al identificador + 1
fTokenID := tkUnknown; //identificador común
end;
procedure TSynMiColor.ProcD;
begin
while Identifiers[linAct[posFin]] do inc(posFin);
fStringLen := posFin - posIni - 1; //calcula tamaño - 1
fToIdent := linAct + posIni + 1; //puntero al identificador + 1
if KeyComp('E') then fTokenID := tkKey else
fTokenID := tkUnknown; //identificador común
end;
procedure TSynMiColor.ProcE;
begin
while Identifiers[linAct[posFin]] do inc(posFin);
fStringLen := posFin - posIni - 1; //calcula tamaño - 1
fToIdent := linAct + posIni + 1; //puntero al identificador + 1
if KeyComp('N') then fTokenID := tkKey else
if KeyComp('ND') then begin fTokenID := tkKey; EndCodeFoldBlock(); end else
procedure TSynMiColor.ProcNull;
//Procesa la ocurrencia del caracter #0
begin
fTokenID := tkNull; //Solo necesita esto para indicar que se llegó al final de la
línae
end;
procedure TSynMiColor.ProcSpace;
//Procesa caracter que es inicio de espacio
begin
fTokenID := tkSpace;
repeat
Inc(posFin);
until (linAct[posFin] > #32) or (linAct[posFin] in [#0, #10, #13]);
end;
procedure TSynMiColor.ProcUnknown;
begin
inc(posFin);
while (linAct[posFin] in [#128..#191]) OR // continued utf8 subcode
((linAct[posFin]<>#0) and (fProcTable[linAct[posFin]] = @ProcUnknown)) do
inc(posFin);
fTokenID := tkUnknown;
end;
procedure TSynMiColor.Next;
begin
posIni := PosFin; //apunta al primer elemento
if fRange = rsComment then
CommentProc
else
begin
fRange := rsUnknown;
fProcTable[linAct[PosFin]]; //Se ejecuta la función que corresponda.
end;
end;
{Las siguientes funciones, son usadas por SynEdit para el manejo de las
llaves, corchetes, parentesis y comillas. No son cruciales para el coloreado
de tokens, pero deben responder bien.}
function TSynMiColor.GetToken: String;
begin
Result := '';
end;
function TSynMiColor.GetTokenPos: Integer;
begin
Result := posIni - 1;
end;
function TSynMiColor.GetTokenKind: integer;
begin
Result := 0;
end;
procedure TSynMiColor.CommentProc;
begin
fTokenID := tkComment;
case linAct[PosFin] of
#0:
begin
ProcNull;
exit;
end;
end;
while linAct[PosFin] <> #0 do
case linAct[PosFin] of
'*':
if linAct[PosFin + 1] = '/' then
initialization
CreaTablaIdentif; //Crea la tabla para búsqueda rápida
end.
Hasta ahora podemos resumir que para controlar el plegado se requiere de los métodos
StartCodeFoldBlock() y EndCodeFoldBlock(), que tienen las siguientes declaraciones:
No hay mayor magia en esto. Cada llamada a EndCodeFoldBlock(), eliminará siempre el último
plegado agregado con StartCodeFoldBlock().
StartCodeFoldBlock(nil);
EndCodeFoldBlock();
Esta forma de trabajo sería suficiente para un plegado simple. Sin embargo, podríamos necesitar
saber cuál es el bloque que estamos manejando para poder decidir si es válido o no, cerrar el bloque
actual. Por ejemplo sabemos que en Pascal la palabra reservada UNTIL, cierra el bloque REPEAT, pero no
un bloque BEGIN.
Para poder saber qué bloque es el que estamos manejando, podríamos crear una estructura
auxiliar, como una cola (ya que los bloques de plegado se pueden anidar), y poder leer siempre cual es
el último bloque en el que nos encontramos.
Para identificar a nuestro bloque de plegado actual, debemos usar el parámetro “ABlockType” de
StartCodeFoldBlock().
Para ello, podría resultar conveniente tener una lista de identificadores de bloques de plegado en
nuestro resaltador:
TMisTipoDeBloques = (
cfbt_BeginEnd,
cfbt_RepeatUntil,
cfbt_RecordEnd,
cfbt_uses,
cfbt_var
);
StartCodeFoldBlock(Pointer(PtrInt(ABlockType)));
Cerrar este bloque no ofrece ninguna diferencia a cualquier otro bloque. Bastaría una simple
llamada a EndCodeFoldBlock():
EndCodeFoldBlock();
No es necesaria mayor información, porque se sabe que siempre será el último bloque, el que se
cerrará.
Para saber, cuál es el último bloque en la pila de plegados, contamos con el método
TopCodeFoldBlockType():
El parámetro es opcional, y por lo general debe quedarse en cero, a menos que se desee obtener
otro bloque de plegado más interior.
var
p: Pointer;
...
p := TopCodeFoldBlockType; //lee último bloque ingresado
//verifica si corresponde cerrar el bloque actual
if TMisTipoDeBloques(PtrUInt(p)) in [cfbt_BeginEnd, cfbt_RecordEnd] then
EndCodeFoldBlock(DecreaseLevel);
...
Esto puede resultar confuso. Alguien podría preguntar ¿Por qué haría tal cosa? ¿Por qué crearía
un plegado que no es visible? ¿Qué utilidad tendría un bloque de plegado que no se pueda plegar?
La respuesta sencilla sería: Porque de esta forma se permite habilitar o deshabilitar el plegado sin
alterar la estructura de un texto.
De la misma forma, para ser consistente, se debe poner siempre “DecreaseLevel” a FALSE, en
EndCodeFoldBlock(), cuando se ha creado el bloque con “IncreaseLevel” en FALSE.
El objeto “CodeFoldRange”, funciona como una pila a la que se le van agregando (Add) y quitando
la información de plegado.
TSynCustomHighlighterRange = class
private
FCodeFoldStackSize: integer; // EndLevel
FMinimumCodeFoldBlockLevel: integer;
FRangeType: Pointer;
FTop: TSynCustomCodeFoldBlock;
public
constructor Create(Template: TSynCustomHighlighterRange); virtual;
destructor Destroy; override;
function Compare(Range: TSynCustomHighlighterRange): integer; virtual;
function Add(ABlockType: Pointer = nil; IncreaseLevel: Boolean = True):
TSynCustomCodeFoldBlock; virtual;
procedure Pop(DecreaseLevel: Boolean = True); virtual;
function MaxFoldLevel: Integer; virtual;
procedure Clear; virtual;
procedure Assign(Src: TSynCustomHighlighterRange); virtual;
procedure WriteDebugReport;
property FoldRoot: TSynCustomCodeFoldBlock read FTop write FTop;
public
property RangeType: Pointer read FRangeType write FRangeType;
property CodeFoldStackSize: integer read FCodeFoldStackSize;
property MinimumCodeFoldBlockLevel: integer
read FMinimumCodeFoldBlockLevel write FMinimumCodeFoldBlockLevel;
property Top: TSynCustomCodeFoldBlock read FTop;
end;
Este objeto, funciona a manera de pila con respecto a los plegados. El método Add(), agrega un
nuevo elemento y Pop(), extrae el último elemento.
El método Pop(), tiene una protección a intentar eliminar un plegado que no existe. Así que
podría ejecutarse sin temor a desbordar a “CodeFoldRange”.
Para ver el tamaño de la pila, se puede llamar en todo momento a “CodeFoldStackSize”. Este
contador solo se actualizará cuando se llame a StartCodeFoldBlock()con el parámetro “IncreaseLevel” en
TRUE o a EndCodeFoldBlock() con el parámetro “DecreaseLevel” en TRUE, como es la forma por defecto.
TSynCustomFoldHighlighter = class(TSynCustomHighlighter)
protected
// Fold Config
FFoldConfig: Array of TSynCustomFoldConfig;
function GetFoldConfig(Index: Integer): TSynCustomFoldConfig; virtual;
procedure SetFoldConfig(Index: Integer; const AValue:
TSynCustomFoldConfig); virtual;
function GetFoldConfigCount: Integer; virtual;
function GetFoldConfigInternalCount: Integer; virtual;
function GetFoldConfigInstance(Index: Integer): TSynCustomFoldConfig;
virtual;
procedure InitFoldConfig;
procedure DestroyFoldConfig;
procedure DoFoldConfigChanged(Sender: TObject); virtual;
private
...
protected
...
public
property FoldConfig[Index: Integer]: TSynCustomFoldConfig
read GetFoldConfig write SetFoldConfig;
property FoldConfigCount: Integer read GetFoldConfigCount;
end;
Estas propiedades nos proporcionan una estructura interna, para el almacenamiento de las
propiedades de configuración del plegado, y expone además, las propiedades FoldConfig y
FoldConfigCount, para acceder a estas propiedades.
TSynCustomFoldConfig = class(TPersistent)
private
FEnabled: Boolean;
FFoldActions: TSynFoldActions;
FModes: TSynCustomFoldConfigModes;
FOnChange: TNotifyEvent;
FSupportedModes: TSynCustomFoldConfigModes;
...
published
property Enabled: Boolean read FEnabled write SetFEnabled;
property Modes: TSynCustomFoldConfigModes read FModes write SetModes
default [fmFold];
end;
La propiedad más importante es quizá “Enabled”, porque nos permite decidir cuándo un rango de
plegado está o no habilitado.
Esta estructura es una base con las propiedades básicas para un rango de plegado. Para tener una
idea de cómo se utilizan, se puede ir al entorno de Lazarus, y seleccionar el menú
“Herramientas>Opciones>Editor>Plegado de Código>”:
En este caso, se están mostrando los bloques de plegado para el resaltador de Pascal de Lazarus.
Cuando se activa o desactiva alguna de estas casillas, se está cambiando la propiedad “Enabled”, de uno
de los objetos “TSynCustomFoldConfig” que existen en el resaltador.
procedure TSynLFMSyn.EndLfmCodeFoldBlock;
var
DecreaseLevel: Boolean;
begin
DecreaseLevel := TopCodeFoldBlockType < CountLfmCodeFoldBlockOffset;
EndCodeFoldBlock(DecreaseLevel);
end;
En este código, se usa el truco de dar a “p”, un valor desplazado del valor original, cuando se usa
un plegado no visible, en StartLfmCodeFoldBlock(). De modo que, luego se pueda recuperar su valor
original y de paso saber si el plegado era visible o no, en EndLfmCodeFoldBlock().
Por norma, se espera que existan tantos objetos TSynCustomFoldConfig, como bloques de
plegado distintos. Así por ejemplo, en el resaltador SynLFMSyn, existen los siguientes bloques:
TLfmCodeFoldBlockType = (
cfbtLfmObject, // object, inherited, inline
cfbtLfmList, // <>
cfbtLfmItem, // Item
cfbtLfmNone
);
Estos métodos se ejecutan al inicio, para crear las estructuras de configuración en FFoldConfig[].
Como hemos visto, todos los resaltadores que deseen implementar plegado de código, deben
derivar de la clase TSynCustomFoldHighlighter en lugar de TSynCustomHighlighter.
procedure primero;
begin
max := 0;
if a>b then
begin
max := a;
end
else
begin
max := b;
end;
end;
Como es común, podemos ahorrar unas líneas uniendo algunas palabras claves en la misma
línea:
procedure primero;
begin
max := 0;
if a>b then begin
max := a;
end else begin
max := b;
end;
end;
En este código se tiene que el cuerpo del IF termina en la misma línea en donde empieza el
cuerpo del ELSE, por ello, se aprecia una especia de “solapamiento” de los bloques, pero no es así. En
realidad se podría decir que el cuerpo del IF “termina”, después de la palabra reservada END, y que el
cuerpo el ELSE, empieza con la palabra reservada BEGIN.
Como los bloques de plegado, se pueden anidar, entonces estamos introduciendo el concepto
de “nivel”. Si no hemos abierto ningún bloque de plegado, diremos que estamos en el nivel cero.
Cuando se abre un bloque de plegado, diremos que estamos en el nivel 1 y así sucesivamente. Es por
ello que comúnmente, estaremos hablando de “niveles de anidamiento” dentro de un bloque de
plegado.
Estas variables, y otras más se guardan como parte de la información que el resaltador almacena
en cada línea del editor.
Un ejercicio visual nos ayudará a entender cómo se procesa el plegado de código dentro de
TSynCustomFoldHighlighter:
EndLevel MinLevel
procedure primero; 1 0
begin 2 1
max := 0; 2 2
if a>b then begin 3 2
max := a; 3 3
end else begin 3 2
max := b; 3 3
end; 2 2
end; 0 0
En la segunda línea se abre un nuevo bloque con la palabra reservada BEGIN. Para fines de
plegado de código, no es tan importante saber si el bloque empieza antes o después de BEGIN.
Esta información es todo lo que se necesita el editor para mostrar las marcas de plegado en el
panel lateral. Cada vez que se encuentra que EndLevel > MinLevel, significa que se ha abierto uno o más
bloques de plegado, y se debe mostrar la marca de bloque. Si EndLevel, en la línea anterior es mayor
que MinLevel, en una línea, significa que el bloque de la línea anterior se ha cerrado. Esta información
permite realizar exploraciones rápidas en las líneas para obtener información sobre los bloques de
plegado.
Desde luego que TSynCustomFoldHighlighter, maneja mayor información para lidiar con los
bloques, pero para fines de visualización de marcas, esta información sería suficiente, y es parte de la
información que se almacena en cada línea del editor.
Puede parecer que se pierde información del plegado si es que se abre y cierra un bloque,
dentro de una misma línea, pero hay que considerar que para fines de plegado de código, ese bloque
resultaría intrascendente.
Para ver los valores EndLevel y MinLevel, se puede acceder a las propiedades:
Solo es necesario pasar el número de línea requerido (iniciando en cero). El campo “AFilter” no
se usa, así que se debe dejar en NIL.
CurrentLines[], es exactamente igual y permite acceder a las líneas del editor actual. Hasta ahí
todo funciona bien.
El manejo de bloques de plegado, requiere información adicional, así que se ha creado una
estructura especial que permita empaquetar la información de rango (el mismo puntero de siempre), y
adicionalmente otros campos más.
TSynCustomHighlighterRange = class
private
FCodeFoldStackSize: integer; // EndLevel
FMinimumCodeFoldBlockLevel: integer;
FRangeType: Pointer;
FTop: TSynCustomCodeFoldBlock;
public
constructor Create(Template: TSynCustomHighlighterRange); virtual;
destructor Destroy; override;
function Compare(Range: TSynCustomHighlighterRange): integer; virtual;
function Add(ABlockType: Pointer = nil; IncreaseLevel: Boolean = True):
TSynCustomCodeFoldBlock; virtual;
procedure Pop(DecreaseLevel: Boolean = True); virtual;
function MaxFoldLevel: Integer; virtual;
procedure Clear; virtual;
procedure Assign(Src: TSynCustomHighlighterRange); virtual;
procedure WriteDebugReport;
property FoldRoot: TSynCustomCodeFoldBlock read FTop write FTop;
public
property RangeType: Pointer read FRangeType write FRangeType;
property CodeFoldStackSize: integer read FCodeFoldStackSize;
property MinimumCodeFoldBlockLevel: integer
read FMinimumCodeFoldBlockLevel write FMinimumCodeFoldBlockLevel;
property Top: TSynCustomCodeFoldBlock read FTop;
end;
Esto significa que ahora en TSynCustomFoldHighlighter, cuando se lea CurrentRanges[] para una
línea, no leeremos directamente el valor del rango, sino que obtendremos la referencia a un objeto
TSynCustomHighlighterRange, con información adicional (mucha información adicional), necesaria para
el manejo de los bloques de plegado.
TSynCustomFoldHighlighter
CurrentLines[] CurrentRanges[]
0 String 0 TSynCustomHighlighterRange
1 String 1 TSynCustomHighlighterRange
2 String 2 TSynCustomHighlighterRange
3 String 3 TSynCustomHighlighterRange
TSynEdit 4 String 4 TSynCustomHighlighterRange
5 String 5 TSynCustomHighlighterRange
6 String 6 TSynCustomHighlighterRange
7 String 7 TSynCustomHighlighterRange
n String n TSynCustomHighlighterRange
La información de rango, pasa a ser ahora un humilde campo más de este nuevo objeto. El
campo es RangeType. Es por ello que, en un resaltador con plegado, cuando se implementan los
métodos:
TSynFacilSyn.GetRange: Pointer;
TSynFacilSyn.SetRange(Value: Pointer);
, se debe trabajar con el campo “CodeFoldRange.RangeType”, en lugar del valor directo de la función.
p := TSynCustomHighlighterRange(CurrentRanges[SynEdit1.CaretY-1])
Esto es así porque todos los bloques abiertos, con StartCodeFoldBlock(), se almacenan en una
lista enlazada, y no en una pila LIFO, como se podría esperar.
TSynCustomFoldHighlighter
CurrentRanges[]
0 TSynCustomHighlighterRange
1 TSynCustomHighlighterRange property RangeType: Pointer
2 TSynCustomHighlighterRange
3 TSynCustomHighlighterRange
4 TSynCustomHighlighterRange property Top: TSynCustomCodeFoldBlock
5 TSynCustomHighlighterRange
6 TSynCustomHighlighterRange
7 TSynCustomHighlighterRange
n TSynCustomHighlighterRange
TSynCustomCodeFoldBlock = class
private
FBlockType: Pointer;
FParent, FChildren: TSynCustomCodeFoldBlock;
FRight, FLeft: TSynCustomCodeFoldBlock;
FBalance: Integer;
function GetChild(ABlockType: Pointer): TSynCustomCodeFoldBlock;
protected
function GetOrCreateSibling(ABlockType: Pointer):
TSynCustomCodeFoldBlock;
property Right: TSynCustomCodeFoldBlock read FRight;
property Left: TSynCustomCodeFoldBlock read FLeft;
property Children: TSynCustomCodeFoldBlock read FChildren;
public
destructor Destroy; override;
procedure WriteDebugReport;
public
procedure InitRootBlockType(AType: Pointer);
property BlockType: Pointer read FBlockType;
property Parent: TSynCustomCodeFoldBlock read FParent;
property Child[ABlockType: Pointer]: TSynCustomCodeFoldBlock read
GetChild;
end;
Los enlaces a los otros nodos, se encuentran en las propiedades “Parent” y “Children”.
El dato más importante que contiene este nodo es BlockType, que es la referencia el bloque de
plegado que indicamos cuando usamos StartCodeFoldBlock(). Existe un BlockType por cada nodo, es
decir por cada bloque abierto.
Como sucede con el RangeType, el puntero BlockType, puede hacer referencia a un objeto real o
ser un entero o enumerado codificado en un dato de tipo puntero. Esto dependerá de cómo se
implemente el resaltador que haga uso del plegado de código.
, permite devolver la línea final del bloque, al final de la línea ALineIndex (que empieza en 0 para
la primera línea). El segundo parámetro, permite especificar el nivel del bloque a usar. Para el bloque de
mayor nivel, se debe dejar en cero.
1 procedure primero;
2 begin
3 max := 0;
4 if a>b then begin
5 max := a;
6 end else begin
7 max := b;
8 end;
9 end;
El método FoldLineLength(), devuelve el número de líneas del bloque abierto al final de la línea
indicada. Tiene los mismos parámetros que FoldEndLine():
2.7 Autocompletado
1. Está asociado siempre a un editor de tipo SynEdit. Esta asociación se puede hacer en diseño, o
por código.
3. Al activarse, debe aparecer una ventana, en la posición del cursor, con una lista de palabras (o
frases), de las que se debe seleccionar una de ellas.
4. Al mostrarse la ventana de selección, se toma la palabra actual, la que esta antes del cursor,
como palabra de trabajo. Esta es la palabra que será reemplazada.
5. Si al mostrarse la ventana de selección, esta solo tiene una opción, se procederá a remplazar
automáticamente la palabra de trabajo con esta única opción y se cerrará la ventana de
selección.
6. Cuando está mostrada la ventana de selección, se puede seguir escribiendo sobre el editor, pero
la ventana de selección tomará el control de algunas teclas, como las direccionales. Además no
se permitirá eliminar la palabra de trabajo, porque debe tener al menos un carácter.
7. En la lista mostrada, para desplazarse por las opciones se pueden usar las teclas direccionales,
<Pag.Arriba>, <Pag.Abajo>, <Inicio>, o <Fin>. Estas teclas dejarán de actuar sobre el editor,
mientras esté mostrada la lista de opciones.
8. Para seleccionar una de las opciones se debe pulsar la tecla <enter>, <espacio>, o algún símbolo
como ‘+’, o ‘-‘.
10. SI no se quiere seleccionar alguna opción de la ventana de opciones, se debe pulsar <escape>,
para hacer desaparecer la ventana de opciones y continuar la edición normal.
11. Solo se puede hacer desaparecer, la ventana de opciones, seleccionando alguno de sus ítems o
pulsando <escape>.
Una vez que tengamos los componentes de trabajo, se requiere asociar “SynCompletion1” a
“SynEdit1”. Para ello, se configura en el explorador de objetos, la propiedad “Editor” de
“SynCompletion1”, para que apunte a “SynEdit1”:
Observar que la propiedad por defecto para “ShortCut”, es “Ctrl + Espacio”, lo que significa que
esta combinación será la que active la lista desplegable del autocompletado.
Una vez asociada esta propiedad, ya estará lista la funcionalidad para trabajar en el editor. Pero
todavía se necesita agregar la lista de palabras que se mostrará en el menú desplegable. Para ello
podemos modificar directamente desde el Inspector de objetos, la propiedad “ItemList”:
Este método de trabajo, es sencillo y rápido de implementar, pero no permite opciones más
elaboradas de trabajo como filtrar la lista de acuerdo a la palabra que se está escribiendo. Para esta y
otras funcionalidades, es necesario trabajar con código.
La inicialización es sencilla:
procedure Mostrar;
var p:TPoint;
begin
P := Point(ed.CaretXPix,ed.CaretYPix + ed.LineHeight);
P.X:=Max(0,Min(P.X, ed.ClientWidth - TSynCompletion1.Width));
P := ed.ClientToScreen(p);
//Abre menú contextual. Solo se mostrará si tiene elementos.
SynCompletion1.Execute('', p.x, p.y);
end;
El cálculo de p.x y p.y, se hace para que la ventana aparezca en la posición del cursor. Es
importante notar que la ventana, solo se mostrará, si “TSynCompletion1”, tiene elementos en su lista.
SynCompletion1.ItemList.add('Alfa');
SynCompletion1.ItemList.add('Beta');
SynCompletion1.ItemList.add('Gamma');
Las cadenas que se agregan, son las que aparecerán al mostrarse la ventana del
“TSynCompletion”.
Si la lista de palabras va a ser fija, entonces se puede llenar una sola vez al inicio del programa. Si
la lista depende de la palabra actual, o de otra información, entonces se puede hacer uso de uno de los
eventos de “TSynCompletion”. El evento en cuestión es “OnExecute”. Este evento se ejecutará antes de
abrir la lista de palabras para el completado. Aquí se puede aprovechar para elegir la lista de palabras a
llenar. Como ayuda adicional, se puede usar la cadena “SynCompletion1.CurrentString”. Aquí se guarda
el identificador que se encuentra antes del cursor, es decir la palabra de trabajo
Con cada tecla pulsada, mientras está activa la ventana de selección, se actualiza la cadena
“CurrentString”.
Otro evento importante es “OnSearchPosition”. Este evento es llamado cada vez que se pulsa
una tecla mientras se tiene la ventana del “TSynCompletion” abierta. Como parámetro entrega un
entero que indica el elemento seleccionado. El primer elemento es el 0. Este valor se puede cambiar
desde dentro del evento, para elegir otro elemento a seleccionar. Si no usa este evento, por defecto, se
selecciona el elemento que coincida con “CurrentString”.
La propiedad “CurrentString”, indica la palabra actual la que está detrás del cursor. No importa
si hay caracteres delante del cursor, “CurrentString” solo considera los caracteres anteriores, hasta
encontrar un espacio. Esta propiedad solo se actualiza, al invocar el “TSynCompletion” desde teclado. Si
se abre con “Execute()”, “CurrentString” iniciará vacía.
3 APÉNDICE
3.1 Algoritmos para la implementación de resaltadores.
Se pueden implementar de diversas formas. El objetivo principal es que sea de respuesta rápida
y se enfocan sobretodo en la comparación rápida de cadenas. Los algoritmos que más se han ensayado
son:
Este es el algoritmo que se ha usado para implementar la mayoría de los resaltadores pre-
definidos que vienen con Lazarus. Es de procesamiento rápido, pero tiene una implementación
complicada.
Trabaja con una tabla de funciones “fProcTable[]”, que se usa para direccionar a cada carácter,
de acuerdo a su tipo, a una función específica que se encargará de identificar el token analizado.
procedure TSynPerlSyn.MakeMethodTables;
var
I: Char;
begin
for I := #0 to #255 do
case I of
#0: fProcTable[I] := @NullProc;
#1..#9,#11,#12,#14..#32: fProcTable[I] := @SpaceProc;
#10: fProcTable[I] := @LFProc;
#13: fProcTable[I] := @CRProc;
':': fProcTable[I] := @ColonProc;
'#': fProcTable[I] := @CommentProc;
'=': fProcTable[I] := @EqualProc;
'>': fProcTable[I] := @GreaterProc;
'<': fProcTable[I] := @LowerProc;
'0'..'9', '.': fProcTable[I] := @NumberProc;
'$', 'A'..'Z', 'a'..'z', '_': fProcTable[I] := @IdentProc;
'-': fProcTable[I] := @MinusProc;
'+': fProcTable[I] := @PlusProc;
'/': fProcTable[I] := @SlashProc;
'*': fProcTable[I] := @StarProc;
#34: fProcTable[I] := @StringInterpProc;
#39: fProcTable[I] := @StringLiteralProc;
else
fProcTable[I] := @UnknownProc;
end;
end;
Aquí los caracteres alfabéticos, se consideran siempre como inicio de identificadores y se hacen
apuntar a la función IdentProc() que se encargará de extraer el identificador y verificar si se trata de una
palabra clave.
procedure TSynPerlSyn.InitIdent;
var
I: Integer;
begin
for I := 0 to 2167 do
Case I of
109: fIdentFuncTable[I] := @Func109;
113: fIdentFuncTable[I] :=@func113;
196: fIdentFuncTable[I] :=@func196;
201: fIdentFuncTable[I] :=@func201;
204: fIdentFuncTable[I] :=@func204;
207: fIdentFuncTable[I] :=@func207;
209: fIdentFuncTable[I] :=@func209;
211: fIdentFuncTable[I] :=@func211;
230: fIdentFuncTable[I] :=@func230;
...
else
fIdentFuncTable[I] := @AltFunc;
end;
end;
Las funciones direccionadas son de la forma “funcXXX”, donde XXX es el valor “hash“ que
corresponde. Para un grupo de palabras claves, solo existen un número de valores “hash” en el rango.
Las otras entradas solo devuelven el valor “tkIdentifier” (a través de AltFunc), que indica que se
trata de un simple identificador.
13
Este valor se obtiene creando una tabla que asigna un valor a cada letra del alfabeto ingles (usualmente en la
tabla mHashTable[], y aprovechando para llenarla en la función “MakeIdentTable”). Con esta tabla se calcula el
valor que le corresponde a cada palabra clave, sumando el valor de cada letra en el identificador. El valor obtenido
suele depender de las letras del identificador y de su tamaño, pero usualmente no supera a 200 en una sintaxis
normal. Este valor no es único para cada palabra, ya que varias palabras distintas pueden compartir el mismo
valor, pero permite categorizar de manera efectiva a los identificadores para restringir la búsqueda a un grupo
mucho menor.
Si existen varias palabras claves que tienen el mismo valor “hash”, se implementa una
comparación adicional, dentro de la función que corresponda:
Este algoritmo es el que describimos en este documento y se basa en usar el primer carácter de
un identificador como prefijo, para acelerar la detección de palabras claves.
También utiliza una tabla de funciones “fProcTable[]”, que se usa para direccionar a cada
carácter, de acuerdo a su tipo, a una función específica que se encargará de identificar el token
analizado. La diferencia está en que esta tabla apuntará a una función específica para cada carácter
alfabético:
procedure TSynMiColorSF.MakeMethodTables;
var
I: Char;
begin
for I := #0 to #255 do
case I of
'-' : fProcTable[I] := @ProcMinus;
'/' : fProcTable[I] := @ProcSlash;
#39 : fProcTable[I] := @ProcString;
'"' : fProcTable[I] := @ProcString2;
'0'..'9': fProcTable[I] := @ProcNumber;
'A','a': fProcTable[I] := @ProcA;
'B','b': fProcTable[I] := @ProcB;
'C','c': fProcTable[I] := @ProcC;
'D','d': fProcTable[I] := @ProcD;
...
'Z','z': fProcTable[I] := @ProcZ;
'_' : fProcTable[I] := @ProcUnder;
'$' : fProcTable[I] := @ProcMacro;
#13 : fProcTable[I] := @ProcCR;
#10 : fProcTable[I] := @ProcLF;
#0 : fProcTable[I] := @ProcNull;
#1..#9, #11, #12, #14..#32: fProcTable[I] := @ProcSpace;
else fProcTable[I] := @ProcUnknown;
end;
end;
Luego cada función de tipo ProcA() o ProcB(), hace directamente las comparaciones para
identificar las palabras claves:
procedure TSynMiColorSF.ProcA;
begin
while Identifiers[fLine[Run]] do inc(Run);
fStringLen := Run - fTokenPos - 1; //calcula tamaño - 1
fToIdent := Fline + fTokenPos + 1; //puntero al identificador + 1
if KeyComp('nd') then fTokenID := tkKey else
if KeyComp('rray') then fTokenID := tkKey else
fTokenID := tkIdentifier; //identificador común
end;
Resaltador PHP (con algoritmo hash): 1.1seg, 1.1 seg, 1.1 seg.
Resaltador PHP (con algoritmo de prefijo): 1.0 seg, 1.0 seg, 1.0 seg.
Resaltador Perl (con algoritmo hash): 1.84 seg, 1.82 seg, 1.83 seg
Resaltador Perl (con algoritmo de prefijo): 1.68 seg, 1.68 seg, 1.68 seg.
Las pruebas muestran los tiempos que toma procesar un archivo completo un número
determinado de veces. La prueba con Perl, realizaba 5000 exploraciones sencillas a un archivo. La rutina
de exploración tenía esta forma:
hlt.GetTokenAttribute;
end;
end;
end;
Ambas pruebas, se hicieron tomando un archivo fuente en PHP y Perl, como ejemplos. El de Perl
tenía 427 líneas. La comparación se hizo en un Windows de 32 bits, en arquitectura Intel.
Después de realizadas las pruebas, se concluye que el algoritmo de prefijos es por lo general
más rápido que el de funciones “hash”. Es posible que haya casos particulares, en los que esto no sea
cierto, pero siempre se podrá mejorar la implementación del algoritmo de prefijos para que sea
comparativamente más rápido.
Tamaño de código Mayor, por el hecho de requerir una Menor, porque solo usa una tabla de
tabla de funciones adicional y una funciones y comparaciones sencillas.
cantidad grande de funciones.
Posibilidad de usar Muy pocas. Su estructura inherente Más manejable. Al ordenar sus
archivos externos de complica hacerlo dinámico. identificadores, permite adaptarlo
sintaxis. mejor al uso de archivos externos de
sintaxis.
Aquí quiero indicar algunos puntos a tener en cuenta en la implementación de algoritmos para
los resaltadores, considerando que estamos usando el compilador Free Pascal en su versión 2.6.2.
Algunos de estos criterios, podrían no ser ciertos en otros compiladores o inclusive en otras versiones de
Free Pascal.
El primer punto que debemos considerar es que las cadenas de tipo PChar, son generalmente
más rápidas que las de tipo String, ya que se manejan como punteros.
Otra consideración con respecto a punteros es que, si se tiene una cadena de tipo PChar,
declarada así:
fLine: PChar;
En los resaltadores, es común tener que avanzar un puntero mientras el carácter apuntado
pertenezca a un conjunto válido de caracteres.
Aquí se supone que el arreglo CharsIdentif[], ha sido inicializado para los caracteres que se
desean considerar como válidos.
Aquí se supone que el conjunto letras, se ha iniciado con los caracteres que se desean
considerar como válidos.
Una tabla comparativa muestra, las velocidades de respuesta de estos tres métodos, ensayados
en Windows con arquitectura x86 de 32 bits:
MÉTODO TIEMPO
Matriz de valores booleanos 39
Comparación con conjuntos 48
Comparar con Case ... Of 45
Se puede ver que la comparación usando matriz de booleanos, es cerca de un 20% más rápida
que la comparación usando conjuntos y cerca de 13% más rápida que usando Case ..Of.
Se ensayó también una comparación usando una matriz de números enteros, en lugar de
valores booleanos, obteniéndose un retardo mayor. Usar enteros es cerca de 1% o 2% más lento.
ÍNDICE